正文
一、什么是ThreadLocal
ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。当线程结束后,每个线程所拥有的那个本地值会被释放。在多线程并发操作“线程本地变量"的时候,线程操作自己的变量副本,从而规避了线程安全问题,是一种空间换时间的思想。
二、ThreadLocal的使用
public class ThreadLocalDemo { // private ThreadLocal<String> threadLocal= ThreadLocal.withInitial(()->{ // return "test threadlocal"; //在定义ThreadLocal的时候设置一个获取初始值的回调函数。 // }); private static ThreadLocal<String> threadLocal=new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException { ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo(); Thread thread = new Thread(() -> { threadLocal.set("test"); System.out.println(threadLocal.get()); }, "t1"); thread.start(); thread.join(); String s = threadLocal.get(); System.out.println("主线程获取不到在thread线程设置的值:"+s); } }
使用场景
线程隔离,ThreadLocal中的数据只属于当前线程,其本地值对其他线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。例如用户会话信息,数据库连接,Session数据管理。
跨函数传递数据,在同一个线程跨类、跨方法传递数据时,如果不用ThreadLocal,就需要靠方法的返回值或者参数传参,这样就增加了代码的耦合度。而ThreadLocal在当前线程设置值以后,在此线程中任何地方都能直接获取到值。避免通过参数传递数据降低代码耦合度。例如spring的编程式事务。
三、ThreadLocal源码分析
set()方法解读
public void set(T value) { //获取当前线程 Thread t = Thread.currentThread(); //获取当前线程的ThreadLocalMap ThreadLocal.ThreadLocalMap map = getMap(t); if (map != null) { //value被绑定到threadLocal实例 map.set(this, value); } else { //没有ThreadLocalMap则创建一个ThreadLocalMap实例,关联到thread实例 createMap(t, value); } }
结:set()步骤
获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量。
如果map不为空,就将value设置到map中,当前的ThreadLocal作为key。
如果map为空,为该线程创建map,然后设置第一个“key-value对”,key为当前ThreadLocal的实例,value为set()方法参数的值。
get()方法解读
public T get() { //获取当前线程 Thread t = Thread.currentThread(); //获取ThreadLocalMap ThreadLocal.ThreadLocalMap map = getMap(t); if (map != null) { //如果不为空,以threadlocal实例为key获取值 ThreadLocal.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(); ThreadLocal.ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value);//绑定值 } else { createMap(t, value);//创建threallocalmap } if (this instanceof TerminatingThreadLocal) { //当线程终止并且已在终止线程中初始化时(即使已使用空值初始化)会收到通知。 TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this); } return value; }
小结:get()大致步骤
首先获取当前线程对象t, 然后从线程t中获取到ThreadLocalMap的成员属性threadLocals
如果当前线程的threadLocals已经初始化(即不为null) 并且存在以当前ThreadLocal对象为Key的值, 则直接返回当前线程要获取的对象;
如果当前线程的threadLocals已经初始化(即不为null)但是不存在以当前ThreadLocal对象为Key的的对象, 那么重新创建一个对象, 并且添加到当前线程的threadLocals Map中,并返回
如果当前线程的threadLocals属性还没有被初始化, 则重新创建一个ThreadLocalMap对象, 并且创建一个对象并添加到ThreadLocalMap对象中并返回。
ThreadLocal实现线程隔离的原理其实就是用了Map的数据结构给当前线程缓存了变量的值, 要使用的时候就从本线程的threadLocals对象中获取就可以了, key就是当前线程。当然在当前线程下获取当前线程里面的Map里面的对象并操作肯定没有线程并发问题了, 当然能做到变量的线程间隔离了。
remove()方法解读
remove()方法用于在当前线程的ThreadLocalMap中移除线程本地变量所对应的值
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) { m.remove(this); } }
private void remove(ThreadLocal<?> key) { ThreadLocal.ThreadLocalMap.Entry[] tab = table; int len = tab.length; //entry的长度 int i = key.threadLocalHashCode & (len-1);//key在数组上的槽点 for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.refersTo(key)) { //如果e等于key值就删除该值,这是一个native方法,方便垃圾回收 e.clear(); expungeStaleEntry(i); return; } } }
ThreadLocalMap解读
ThreadLocalMap内部静态类Entry实现 内部使用table数组存储Entry,默认大小INITIAL_CAPACITY(16) 参数说明: size:table中元素的数量。 threshold:table大小的2/3,当size >= threshold时,遍历table并删除key为null的元素, 如果删除后size >= threshold*3/4时,需要对table进行扩容。 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
可以看到
它并没有实现Map接口;
它没有public的方法, 最多有一个default的构造方法, 因为这个ThreadLocalMap的方法仅仅在ThreadLocal类中调用, 属于静态内部类;
ThreadLocalMap的Entry实现继承了WeakReference<ThreadLocal<?>>
该方法仅仅用了一个Entry数组来存储Key, Value; Entry并不是链表形式, 而是每个bucket里面仅仅放一个Entry。ThreadLocalMap 并不是我们所说的传统的map结构。
Entry对ThreadLocal使用了弱引用(WeakReference,弱引用指向的对象只能生存到下一次垃圾回收之前,也就是说当发生GC回收时,不管内存够不够弱引用的对象都会被回收)。
为什么要使用弱引用呢?如下代码
public void a (){ ThreadLocal<String> threadLocal = new ThreadLocal<>(); threadLocal.set("test");//设置值 threadLocal.get();//获取值 //结束 }
线程执行方法a()时,会创建一个ThreadLocal实例,这个是强引用,在调用set("test")之后,ThreadLocalMap会新建一个Entry实例,这个key是以弱引用的方式指向ThreadLocal的,执行完方法之后,栈帧被销毁,强引用的值也就没有了,但是ThreadLocal的实例还有Entry对应的引用,如果是强引用那么ThreadLocal和value值都不能会GC回收。从而会导致内存泄露问题。
set()方法解读
private void set(ThreadLocal<?> key, Object value) { ThreadLocal.ThreadLocalMap.Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.refersTo(key)) { //如果key等于条目的值,那就直接替换掉旧值 e.value = value; return; } if (e.refersTo(null)) { //如果不相等,调用replaceStaleEntry方法创建新值 replaceStaleEntry(key, value, i); return; } } tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) //如果清理完无用条目(ThreadLocal被回收的条目)、并且数组中的数据大小 >= 阈值的时候对当前的Table进行重新哈希 rehash(); }
getEntry()方法解读
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.refersTo(key)) //如果相等直接返回值 return e; else //如果没有匹配的值,就去后面的条目中查找 return getEntryAfterMiss(key, i, e); }
四、ThreadLocal导致内存泄漏
内存泄漏就是不再使用的内存不能得到回收,引发的最终结果是内存溢出。
在什么情况下ThreadLocal会引发内存泄漏呢?
1、如果一个线程长时间运行而不被销毁,比如线程池。因为对于线程池里面不会销毁的线程, 里面总会存在着threadlocal的强引用, 因为final static 修饰的 ThreadLocal 并不会释放, 而ThreadLocalMap 对于 Key 虽然是弱引用, 但是强引用不会释放, 弱引用当然也会一直有值, 同时创建的value对象也不会释放, 就造成了内存泄露。
2、ThreadLocal应用被设置为null之后,且后续在同一Thread实例执行期间,没有发生对其他threadLocal实例的get(),set(),remove()操作(ThreadLocalMap 在执行这些方法时会清空key为null的Entry)。
如何避免ThreadLocal导致的内存泄漏?
1、尽量使用private static final修饰ThreadLocal实例,使用private和final主要是尽可能不让其他的类修改ThreadLocal的引用,static保证ThreadLocal实例的全局唯一。
2、ThreadLocal 使用完之后务必调用remove()方法。
参考
《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著