ThreadLocal 实现原理
在ThreadLocal
的get(),set()
的时候都会清除线程ThreadLocalMap
里所有key
为null
的value
。而ThreadLocal的remove()方法会先将Entry中对key的弱引用断开,设置为null,然后再清除对应的key为null的value。
每一个Thread
维护一个ThreadLocalMap
映射表,映射表的key
是ThreadLocal
实例,并且使用的是ThreadLocal
的弱引用 ,value是具体需要存储的Object
。下面用一张图展示这些对象之间的引用关系,实心箭头表示强引用,空心箭头表示弱引用。
ThreadLocal local = new ThreadLocal(); local.set("当前线程名称:"+Thread.currentThread().getName());//将ThreadLocal作为key放入threadLocals.Entry中 Thread t = Thread.currentThread();//注意断点看此时的threadLocals.Entry数组刚设置的referent是指向Local的,referent就是Entry中的key只是被WeakReference包装了一下 local = null;//断开强引用,即断开local与referent的关联,但Entry中此时的referent还是指向Local的,为什么会这样,当引用传递设置为null时无法影响传递内的结果 System.gc();//执行GC t = Thread.currentThread();//这时Entry中referent是null了,被GC掉了,因为Entry和key的关系是WeakReference,并且在没有其他强引用的情况下就被回收掉了 //如果这里不采用WeakReference,即使local=null,那么也不会回收Entry的key,因为Entry和key是强关联 //但是这里仅能做到回收key不能回收value,如果这个线程运行时间非常长,即使referent GC了,value持续不清空,就有内存溢出的风险 //彻底回收最好调用remove //即:local.remove();//remove相当于把ThreadLocalMap里的这个元素干掉了,并没有把自己干掉 System.out.println(local);
使用InheritableThreadLocal,ThreadLocal threadLocal = new InheritableThreadLocal()
,这样在子线程中就可以通过get
方法获取到主线程set
方法设置的值了。
ThreadLocalMap
结构
static class ThreadLocalMap { /** * 键值对实体的存储结构 */ static class Entry extends WeakReference<ThreadLocal<?>> { // 当前线程关联的 value,这个 value 并没有用弱引用追踪 Object value; /** * 构造键值对 * * @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用 * @param v v 作 value */ Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // 初始容量,必须为 2 的幂 private static final int INITIAL_CAPACITY = 16; // 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂 private Entry[] table; // ThreadLocalMap 元素数量 private int size = 0; // 扩容的阈值,默认是数组大小的三分之二 private int threshold; }
跟 Java
不同的是,采用的是线性探测法。
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // 以上代码,将entry的value赋值为null,这样方便GC时将真正value占用的内存给释放出来;将entry赋值为null,size减1,这样这个slot就又可以重新存放新的entry了 // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); // 从staleSlot后一个index开始向后遍历,直到遇到为null的entry (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { // 如果entry的key为null,则清除掉该entry e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { // key的hash值不等于目前的index,说明该entry是因为有哈希冲突导致向后移动到当前index位置的 tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) // 对该entry,重新进行hash并解决冲突 h = nextIndex(h, len); tab[h] = e; } } } return i; // 返回经过整理后的,位于staleSlot位置后的第一个为null的entry的index值 }
在从第i个entry向后遍历的过程中,找到对应的key的entry就直接返回,如果遇到key为null的entry,则调用expungeStaleEntry
方法进行清理。
ThreadLocalMap set方法
private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
ThreadLocalMap remove 方法
找到准确的key对应的entry之后,调用Entry的clear方法,紧接着调用expungeStaleEntry,对key为null的entry进行清理。
private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { // 考虑到可能的哈希冲突,一定要准确找到此key对应的entry e.clear(); // 调用Entry的clear方法,见代码2 expungeStaleEntry(i); // 又是这个清除key为null的entry的方法,见代码3 return; } } }
平时怎么使用
看个测试代码
package thread; public class ThreadLocalDemo { /** * ThreadLocal变量,每个线程都有一个副本,互不干扰 */ public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>(); public static void main(String[] args) throws Exception { new ThreadLocalDemo().threadLocalTest(); } public void threadLocalTest() throws Exception { // 主线程设置值 THREAD_LOCAL.set("mainthreadvalue");// 设置的key 是主线程 String v = THREAD_LOCAL.get(); System.out.println("Thread-0线程执行之前," + Thread.currentThread().getName() + "线程取到的值:" + v); new Thread(new Runnable() { @Override public void run() { String v = THREAD_LOCAL.get(); // 获取的是key 是当前线程 渠道的是 null System.out.println(Thread.currentThread().getName() + "线程取到的值:" + v); // 设置 threadLocal THREAD_LOCAL.set("inThreadValue"); // 设置的 key 是当前咸亨 v = THREAD_LOCAL.get(); System.out.println("重新设置之后," + Thread.currentThread().getName() + "线程取到的值为:" + v); System.out.println(Thread.currentThread().getName() + "线程执行结束"); } }).start(); // 等待所有线程执行结束 Thread.sleep(3000L); v = THREAD_LOCAL.get(); System.out.println("Thread-0线程执行之后," + Thread.currentThread().getName() + "线程取到的值:" + v); } }
执行结果:
Thread-0线程执行之前,main线程取到的值:mainthreadvalue Thread-0线程取到的值:null 重新设置之后,Thread-0线程取到的值为:inThreadValue Thread-0线程执行结束 Thread-0线程执行之后,main线程取到的值:mainthreadvalue
面试指南
面试官:那ThreadLocal中弱引用导致的内存泄漏是如何发生的?
小白:如果ThreadLocal没有外部强引用,当发生垃圾回收时,这个ThreadLocal一定会被回收(弱引用的特点是不管当前内存空间足够与否,GC时都会被回收),这样就会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value,并且如果当前线程一直存活,那么就会存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,导致value对应的Object一直无法被回收,产生内存泄露。
面试官:如何解决?
小白:查看源码会发现,ThreadLocal的get、set和remove方法都实现了对所有key为null的value的清除,但仍可能会发生内存泄露,因为可能使用了ThreadLocal的get或set方法后发生GC,此后不调用get、set或remove方法,为null的value就不会被清除。解决办法是每次使用完ThreadLocal都调用它的remove()方法清除数据,或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。