相关代码来自java 8
ThreadLocal一直给人一种奇怪的感觉:既是多线程共享的变量,每个线程有拥有独立的变量值
可以看这段测试代码:
1 | private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() { |
以上可能输出(顺序不一定):
1 | thread1: 1 |
简单来说:ThreadLocal并不存储值,也并不是一个存储值的变量;一个ThreadLocal实例,作为共享变量关联到多个线程,每个线程中,都通过Map的数据结构,建立一个线程私有的、以这一ThreadLocal实例为key的存储,从而可以使用这一ThreadLocal实例查询、操作线程私有的Map,使用了多个ThreadLocal对象时,就相当于Map中多个节点。所以,ThreadLocal实例是作为key共享的,但是每个线程的Map映射值是线程私有的
给人奇怪的感觉只是因为,ThreadLocal了提供的方法名叫set/get,给人的感觉是ThreadLocal实例负责存储值
线程的ThreadLocalMap
每个线程维护了一个ThreadLocalMap实例threadLocals
ThreadLocalMap及内部的Entry类,与HashMap的数据结构类似,实现了一个Map:
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
Entry是一个WeakReference的子类,维护一个对ThreadLocal对象的弱引用,并使用这一ThreadLocal对象作为key
可以看出,Entry和HashMap的Node不同:Entry不包括下一个Entry的指针,所以ThreadLocalMap是使用Entry[]进行存储,而HashMap是数组+链表的形式;在发生冲突时,Entry查找数组的下一个位置直到找到一个可用的位置,即线性探查法,而HashMap是链地址法
在ThreadLocal类set()方法中,通过Thread.currentThread()取得了Thread对象,Thread对象的threadLocals field存储了这一线程关联的ThreadLocalMap,以this(调用方法的ThreadLocal对象的引用)作为key添加到Map中
1 | public void set(T value) { |
在ThreadLocal类get()方法类似,也是使用this引用(调用的ThreadLocal对象的引用)进行查找
1 | private Entry getEntry(ThreadLocal<?> key) { |
首先计算出key在Entry数组中的索引,如果查找到那么直接返回,否则,使用getEntryAfterMiss()方法,依次查找
为什么使用ThreadLocal对象的弱引用作为ThreadLocalMap中Entry的key
弱引用,即对象只存在一个弱引用时,GC时会直接回收
在ThreadLocalMap的Entry中,使用弱引用来关联ThreadLocal对象,这样ThreadLocalMap不会影响ThreadLocal对象的回收,不然,如果使用了强引用,可能在外部调用者释放了ThreadLocal对象的引用后,因为ThreadLocalMap持有一个强引用,ThreadLocal对象将继续存活
但是使用弱引用也带了问题:如果外部调用者释放了ThreadLocal对象的引用,那么接下来的依次GC将回收这一ThreadLocal对象,ThreadLocalMap中Entry的key将指向一个null,但是相应的Entry对象和value将继续存在,引起内存泄漏
为了解决这一问题,在对线程关联的其他ThreadLocal进行get、set时(已被回收threadLocal不存在引用可以调用方法了,所以要等线程关联的其他ThreadLocal调用),会直接或间接的调用expungeStaleEntry()方法,释放key为null的Entry和相应value。但是这种方式也需要等到调用expungeStaleEntry()时才可能释放被回收的ThreadLocal关联的Entry、value
在ThreadLocal类的文档中提到“ThreadLocal instances are typically private static fields”,在将ThreadLocal声明为static变量时,可以从外部更好的控制对ThreadLocal的引用,不会因为作用域等失去引用、导致ThreadLocalMap中的内存泄漏
关于ThreadLocal的子类InheritableThreadLocal
ThreadLocal可以用于在线程生命周期内传递变量,但是在异步任务、子线程等情况下,ThreadLocal不能继续传递。InheritableThreadLocal类即解决这一问题,在创建子线程时,会将父线程持有的InheritableThreadLocal对象传递到子线程
这个过程主要是:在线程init()方法中:
1 | if (inheritThreadLocals && parent.inheritableThreadLocals != null) |
其中调用的关键代码是:
1 | Entry e = parentTable[j]; |
将父线程的key(即InheritableThreadLocal对象)传入子线程的map作为key,但是value是调用了key(InheritableThreadLocal)对象的childValue()方法来得到一个value,将返回的value作为子线程中这一key对应的值,所以,子线程中持有的值,其实是InheritableThreadLocal中childValue()方法返回的,这一方法默认直接将传入的父线程的v返回,所以默认情况下子线程将和父线程持有同一个value
这种情况下,如果value是一个可变对象(比如Date),那么子线程对它的修改,父线程也是可见的;如果是SimpleDateFormat这样的非线程安全对象,那么子线程、父线程并发使用这一value时会存在线程安全问题
如果重写了InheritableThreadLocal类的childValue()方法,根据父线程的value的值new一个新的对象返回,或者是使用clone()方法(但是clone()方法默认是浅复制),那么子线程将得到一个和父线程值相同的另一个对象实例,自然也不会出现共享实例带来的问题,相当于为每个线程new一个新的对象
所以感觉,InheritableThreadLocal类还是适合传递不可变的对象