我们创建的变量是可以被任何⼀个线程访问并修改的。而ThreadLocal 类主要解决的就是让每个线程绑定⾃⼰的值,可以将 ThreadLocal 类形象的⽐喻成存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。
ThreadLocal类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量(这个变量不能是共享变量本身)。
概念引入 ThreadLocal的常用方法
方法声明
描述
ThreadLocal()
创建ThreadLocal对象
initialValue()
1. 返回当前线程对应的”初始值”,只在调用get 的时候才触发(延迟加载 ) 2. 如果调用get方法前调用了set方法,不会调用此方法 3. 一般情况每个线程最多调用一次本方法,除非他调用了remove() 4. 不重写此方法,默认返回null
public void set( T value)
设置当前线程绑定的局部变量
public T get()
获取当前线程绑定的局部变量: 1. 取出当前线程的ThreadLocalMap,然后调用map.getEnty,获取本ThreadLocal的value 2. ThreadLocalMap以及其key/value都是保存在Thread中,而不是ThreadLocal
public void remove()
移除当前线程绑定的局部变量
ThreadLocal结构
注意 :ThreadLocalMap中的key为弱引用,这里先不纠结,后面会介绍
Java的四种引用类型 :
强引用 :我们常常new出来的对象就是强引用类型 ,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
软引用 :使用SoftReference修饰的对象被称为软引用 ,软引用指向的对象在内存要溢出的时候被回收
弱引用 :使用WeakReference修饰的对象被称为弱引用 ,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
虚引用 :虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
简单案例 普通多线程 首先我们先看一段简单的多线程示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class MyDemo { private String content; private String getContent () { return content; } private void setContent (String content) { this .content = content; } public static void main (String[] args) { MyDemo demo = new MyDemo(); for (int i = 0 ; i < 5 ; i++) { Thread thread = new Thread(new Runnable() { @Override public void run () { demo.setContent(Thread.currentThread().getName() + "的数据" ); System.out.println("-----------------------" ); System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent()); } }); thread.setName("线程" + i); thread.start(); } } } ----------------------- ----------------------- ----------------------- 线程2 --->线程4 的数据 ----------------------- 线程1 --->线程4 的数据 ----------------------- 线程3 --->线程3 的数据 线程0 --->线程3 的数据 线程4 --->线程4 的数据
从结果可以看出多个线程在访问同一个变量的时候出现的异常,线程间的数据没有隔离。
解决方案 synchronized解决 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class Demo02 { private String content; public String getContent () { return content; } public void setContent (String content) { this .content = content; } public static void main (String[] args) { Demo02 demo02 = new Demo02(); for (int i = 0 ; i < 5 ; i++) { Thread t = new Thread(){ @Override public void run () { synchronized (Demo02.class){ demo02.setContent(Thread.currentThread().getName() + "的数据" ); System.out.println("-------------------------------------" ); String content = demo02.getContent(); System.out.println(Thread.currentThread().getName() + "--->" + content); } } }; t.setName("线程" + i); t.start(); } } } ----------------------- 线程0 --->线程0 的数据 ----------------------- 线程3 --->线程3 的数据 ----------------------- 线程2 --->线程2 的数据 ----------------------- 线程1 --->线程1 的数据 ----------------------- 线程4 --->线程4 的数据
从结果可以发现, 加锁确实可以解决这个问题,但是在这里我们强调的是线程数据隔离的问题,并不是多线程共享数据的问题, 而且加锁也会导致程序的效率变低,所以在这个案例中使用synchronized关键字是不合适的。
ThreadLocal解决 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class MyDemo { static ThreadLocal<String> threadLocal = new ThreadLocal<>(); private String content; private String getContent () { return threadLocal.get(); } private void setContent (String content) { threadLocal.set(content); } public static void main (String[] args) { MyDemo demo = new MyDemo(); for (int i = 0 ; i < 5 ; i++) { Thread thread = new Thread(new Runnable() { @Override public void run () { demo.setContent(Thread.currentThread().getName() + "的数据" ); System.out.println("-----------------------" ); System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent()); } }); thread.setName("线程" + i); thread.start(); } } } ----------------------- 线程0 --->线程0 的数据 ----------------------- 线程1 --->线程1 的数据 ----------------------- 线程2 --->线程2 的数据 ----------------------- 线程3 --->线程3 的数据 ----------------------- 线程4 --->线程4 的数据
从结果来看,这样很好的解决了多线程之间数据隔离的问题,十分方便。
ThreadLocal的优点
每一个Thread内部都有自己的副本,线程之间不共享
无需加锁,提高执行效率
更高效利用内存、节省开销
免去传参烦恼 :在任何方法中都能很方便的获取到该对象,不同多级传递参数,降低了代码耦合度。
ThreadLocal&synchronized对比 虽然ThreadLocal模式与synchronized关键字都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同。
synchronized
ThreadLocal
原理
同步机制采用’以时间换空间’的方式, 只提供了一份变量,让不同的线程排队访问
ThreadLocal采用’以空间换时间’的方式, 为每一个线程都提供了一份变量的副本 ,从而实现同时访问而相不干扰
侧重点
多个线程之间访问资源的同步性
多线程中让每个线程之间的数据相互隔离
场景实战 常见用途 ThreadLocal有两种典型的使用场景,可以概括为:
线程隔离
ThreadLocal 中数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。另外,由于各个线程之间的数据相互隔离,避免同步加锁带来的性能损失,大大提升了并发性的性能。
数据库连接独享、SimpleDateFormat、Random等场景经常需要配合使用。
跨函数传递数据
同一个线程内,跨类、跨方法传递数据时,如果不用 ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。
一般用来传递需要在函数之间频繁传递的数据、HTTP 的用户请求实例 HttpRequest、请求过程中的用户会话(Session)
场景案例 场景一:线程隔离,每个线程都需要一个独享的对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class ThreadLocalTest { public static ExecutorService threadPool = Executors.newFixedThreadPool(10 ); public static void main (String[] args) throws InterruptedException { for (int i = 0 ; i < 1000 ; i++) { int finalI = i; threadPool.submit(() -> { String date = new ThreadLocalTest().date(finalI); System.out.println(date); }); } threadPool.shutdown(); } public String date (int seconds) { Date date = new Date(1000 * seconds); SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get(); return dateFormat.format(date); } } class ThreadSafeFormatter { public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue () { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" ); } }; }
场景二:跨函数传递数据 有时候我们会遇到多层级调用,然后同一基本信息需要不断被传递的场景,如果我们只是简单的将参数在各个方法上传递就会显得代码耦合度很高,也难以维护。这个时候,ThreadLocal也能完美的解决这个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class ThreadLocalTest2 { public static void main (String[] args) { new UserService().getUser(); } } class UserService { public void getUser () { User user = new User("user1" ); UserContextHolder.holder.set(user); new UserInfoService().getUserInfo(); } } class UserInfoService { public void getUserInfo () { User user = UserContextHolder.holder.get(); System.out.println("getUserInfo====>" + user.name); new UserAddress().getAddress(); } } class UserAddress { public void getAddress () { User user = UserContextHolder.holder.get(); System.out.println("getAddress====>" + user.name); } } class UserContextHolder { public static ThreadLocal<User> holder = new ThreadLocal<>(); } class User { String name; public User (String name) { this .name = name; } }
使用方式推荐 由于 ThreadLocal 使用不当会导致严重的内存泄漏,所以为了更好的避免内存泄漏的发生,我们使用 ThreadLocal 时遵守以下两个原则:
(1)尽量使用 private static final 修饰 ThreadLocal 实例。使用 private 与 final 修饰符,主要是尽可能不让他人修改、变更 ThreadLocal 变量的引用; 使用 static 修饰符主要为了确保 ThreadLocal 实例的全局唯一。
(2)ThreadLocal 使用完成之后务必调用 remove 方法。这是简单、有效地避免ThreadLocal引发内存泄漏的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class ThreadLocalDemo01 { public static final Integer MAX_THREAD_NUM = 5 ; private final static ThreadLocal<Product> threadLocal = ThreadLocal.withInitial(Product::new ); static class Product { AtomicInteger num = new AtomicInteger(); public void produce () { num.incrementAndGet(); } } public static void main (String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(MAX_THREAD_NUM); for (int i = 0 ; i < MAX_THREAD_NUM; i++) { int finalI = i; executorService.execute(() -> { for (int j = 0 ; j < finalI + 1 ; j++) { threadLocal.get().produce(); } System.out.println(Thread.currentThread() + ":" + threadLocal.get().num.get()); threadLocal.remove(); }); } } }
源码分析 JDK8 ThreadLocal
的设计是:每个Thread
维护一个ThreadLocalMap
哈希表,这个哈希表的key
是ThreadLocal
实例本身,value
才是真正要存储的值Object
。
(1) 每个Thread线程内部都有一个Map (ThreadLocalMap) (2) Map里面存储ThreadLocal对象(key)和线程的变量副本(value) (3)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。 (4)对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
ThreadLocal核心方法源码 基于ThreadLocal的内部结构,我们继续探究一下ThreadLocal的核心方法源码,更深入的了解其操作原理。
除了构造之外, ThreadLocal对外暴露的方法有以下4个:
方法声明
描述
protected T initialValue()
返回当前线程局部变量的初始值
public void set( T value)
设置当前线程绑定的局部变量
public T get()
获取当前线程绑定的局部变量
public void remove()
移除当前线程绑定的局部变量
get方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 public T get () { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) { ThreadLocalMap.Entry e = map.getEntry(this ); if (e != null ) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } private T setInitialValue () { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) map.set(this , value); else createMap(t, value); return value; } ThreadLocalMap getMap (Thread t) { return t.threadLocals; } void createMap (Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this , firstValue); }
大致流程如下:
先尝试获得当前线程,然后获得当前线程的 ThreadLocalMap 成员,暂存于 map 变量。
如果获得的 map 不为空,以当前 threadlocal 实例为 Key 尝试获得 map 中的 Entry(条目)。
如果 Entry 条目不为空,返回 Entry 中的 Value。
如果 Entry 为空,则通过调用 initialValue 初始化钩子函数获取“ThreadLocal”初始值, 并设置在 map 中。如果 map 不存在,还会给当前线程创建新 ThreadLocalMap 成员,并绑定第一个“Key-Value 对”。
set方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void set (T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) map.set(this , value); else createMap(t, value); }
大致流程如下:
获得当前线程,然后获得当前线程的 ThreadLocalMap 成员,暂存于 map 变量。
如果 map 不为空,则将 Value 设置到 map 中,当前的 Threadlocal 作为 key。
如果 map 为空,给该线程创建 map,然后设置第一个“Key-Value 对”,Key 为当前的ThreadLocal 实例,Value 为 set 方法的参数 value 值。
remove方法 1 2 3 4 5 6 7 8 9 10 11 12 public void remove () { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null ) m.remove(this ); }
initialValue方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 protected T initialValue () { return null ; }
ThreadLocalMap源码分析 ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能(Entry [] table),其内部的Entry也是独立实现。
(1)成员变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private static final int INITIAL_CAPACITY = 16 ;private Entry[] table;private int size = 0 ;private int threshold; private void setThreshold (int len) { threshold = len * 2 / 3 ; }
(2) 存储结构 - Entry
1 2 3 4 5 6 7 8 9 10 11 12 static class Entry extends WeakReference <ThreadLocal > { Object value; Entry(ThreadLocal k, Object v) { super (k); value = v; } }
常见问题 ThreadLocal 内存泄露问题
不再用到的内存,没有及时释放,就叫做内存泄漏。对于持续运行的服务进程,必须及时释放内存,否则内存占用率越来越高,轻则影响 系统性能,重则导致进程崩溃。
由上文我们知道了,ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。所以,如果ThreadLocal 没有被外部强引⽤的情况下,在垃圾回收的时候,key 会被清理掉,⽽ value 不会被清理掉。这样⼀来, ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远⽆法被GC 回收,这个时候就可能会产⽣内存泄露。
ThreadLocalMap实现中已经考虑了这种情况,在调⽤ set() 、remove() 、rehash() ⽅法的时候,会清理掉key为 null 的记录。
但是ThreadLocal不被使用时,这几个方法也不会被调用,这种情况下,线程只要不终止,还是可能出现内存泄露的问题。所以我们使⽤完ThreadLocal ⽅法后,最好⼿动调⽤remove()⽅法,这样就能避免这个问题了。
空指针异常 有时候,我们在调用get方法之前没有set,也没有重写initialValue,这个时候很可能因为自动拆装箱问题导致空指针异常:
1 2 3 4 5 6 7 8 9 10 11 12 public class ThreadLocalTest2 { public static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); public static long get () { return threadLocal.get(); } public static void main (String[] args) { get(); } }