ThreadLocal作用和使用 ThreadLocal是啥? ThreadLocal出现在各种语言中,我们主要关心它在Java中的使用和源码分析。
ThreadLocal的作用是提供线程内的局部变量,简单来说就是在各个线程内部创建一个变量的副本,我们可以观察到每一个Thread实例都有这样一个属性:
1 2 3 /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
其实我们可以在并发环境下,使用各种锁的实现来访问变量,但实际上ThreadLocal的思想就是用空间换时间,每个线程都有一份变量的副本,保证各个线程内部的变量不互相干扰。
ThreadLocal咋用? 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 public class Test { static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); public static void main (String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { System.out.println(Thread.currentThread().getName()); System.out.println(threadLocal.get()); threadLocal.set(0 ); System.out.println(threadLocal.get()); }); Thread t2 = new Thread(() -> { System.out.println(Thread.currentThread().getName()); System.out.println(threadLocal.get()); threadLocal.set(1 ); System.out.println(threadLocal.get()); }); t1.start(); t1.join(); t2.start(); } }
它的运行结果如下:
1 2 3 4 5 6 Thread-0 null 0 Thread-1 null 1
这里我们要注意第二个null,在Thread-0设置完ThreadLocal变量后,Thread-1拿到自己的ThreadLocal变量的值还是初始值null,正是体现了各个线程间的变量隔离。
另外一个要注意的点就是ThreadLocal变量要声明为static的 。
ThreadLocal源码 上面我们看到了每个线程实例都有一个ThreadLocalMap的属性,实际上:
每个线程都维护着一个ThreadLocalMap,用来存放该线程所有的ThreadLocal;
ThreadLocalMap底层是一个Entry[] table
数组,每一个Entry都是一个K-V,key为ThreadLocal,value为存储的值:
1 2 3 4 5 6 7 8 9 static class Entry extends WeakReference <ThreadLocal <?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super (k); value = v; } }
关于Entry这个数据结构,它是继承了WeakReference实现的,当ThreadLocal的引用销毁时(一般是线程结束),指向堆中的ThreadLocal实例的强引用就消失了,只有一条Entry的key指向ThreadLocal实例的弱引用,我们知道弱引用的特性,那么堆中ThreadLocal实例是可以被GC回收的。
此时Entry的key为null,但直到线程结束前,Entry中的value都是无法被回收的,这里就有可能造成内存泄漏 ,后面我们会分析如何解决。
回过头来看下代码吧,ThreadLocal都是怎么实现的。
get()方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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(); }
逻辑清晰,不用过多解释了,下面再看看里面具体的几个方法。
getMap 1 2 3 ThreadLocalMap getMap (Thread t) { return t.threadLocals; }
这个方法很简单,拿到当前线程的threadLocals,也就是最开始我们说的Thread类中定义的那个属性,初始值为null。
setInitialValue 1 2 3 4 5 6 7 8 9 10 11 12 13 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 1 2 3 void createMap (Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this , firstValue); }
createMap()很简单,new一个ThreadLocalMap实例。
ThreadLocalMap是我们分析的重点了,下面的一些源码都是针对这个静态内部类来分析的。首先看一下它的成员变量和定义(去掉源码的一些注释):
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 static class ThreadLocalMap { static class Entry extends WeakReference <ThreadLocal <?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super (k); value = v; } } 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 ; } private static int nextIndex (int i, int len) { return ((i + 1 < len) ? i + 1 : 0 ); } private static int prevIndex (int i, int len) { return ((i - 1 >= 0 ) ? i - 1 : len - 1 ); }
ThreadLocalMap的构造函数,没啥可解释的:
1 2 3 4 5 6 7 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1 ); table[i] = new Entry(firstKey, firstValue); size = 1 ; setThreshold(INITIAL_CAPACITY); }
map.getEntry 1 2 3 4 5 6 7 8 private Entry getEntry (ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1 ); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
主要看一下这个getEntryAfterMiss()方法。
getEntryAfterMiss 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private Entry getEntryAfterMiss (ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null ) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null ) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null ; }
这里我们看一下key为null则清理entry,因为此时这个entry就是一个过期对象。其实这个expungeStaleEntry()方法就是在key为null时,为了避免value还是强引用无法GC,从而造成内存泄漏。其实在每一个get()、set()操作时都会清理这些key为null的entry的。
expungeStaleEntry 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 private int expungeStaleEntry (int staleSlot) { Entry[] tab = table; int len = tab.length; tab[staleSlot].value = null ; tab[staleSlot] = null ; size--; Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null ; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null ) { e.value = null ; tab[i] = null ; size--; } else { int h = k.threadLocalHashCode & (len - 1 ); if (h != i) { tab[i] = null ; while (tab[h] != null ) h = nextIndex(h, len); tab[h] = e; } } } return i; }
主要有两个部分:
expunge entry at staleSlot:将value设置为null,并且Entry的引用也设置为null,GC时就会回收掉;
Rehash until we encounter null:将staleSlot之后、null值之前的这一段做调整。清除key为null的Entry,对于key不为null,做rehash。
做rehash主要是方便下次处理hash冲突,或者尽量避免hash冲突。
set()方法 1 2 3 4 5 6 7 8 public void set (T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) map.set(this , value); else createMap(t, value); }
map.set 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 private void set (ThreadLocal<?> key, Object value) { 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(); }
replaceStaleEntry set()方法中调用了这个方法,它主要是处理key为null时的场景,因为有可能此时的value还不为null。
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 private void replaceStaleEntry (ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null ; i = prevIndex(i, len)) if (e.get() == null ) slotToExpunge = i; for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null ; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return ; } if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } tab[staleSlot].value = null ; tab[staleSlot] = new Entry(key, value); if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
这个方法我们再捋一遍:
第1个for循环:我们向前找到key为null的位置,记录为slotToExpunge,这里是为了后面的清理过程,可以不关注了;
第2个for循环:我们从staleSlot起到下一个null为止,若是找到key和传入key相等的Entry,就给这个Entry赋新的value值,并且把它和staleSlot位置的Entry交换,然后调用CleanSomeSlots()清理key为null的Entry;
若是一直没有key和传入key相等的Entry,那么就在staleSlot处新建一个Entry。函数最后再清理一遍空key的Entry。
rehash rehash()执行的时机是size >= threshold
。
1 2 3 4 5 6 7 private void rehash () { expungeStaleEntries(); if (size >= threshold - threshold / 4 ) resize(); }
清理完key为null的Entry后,如果size >= threshold的3/4,调用resize():
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 private void resize () { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2 ; Entry[] newTab = new Entry[newLen]; int count = 0 ; for (int j = 0 ; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null ) { ThreadLocal<?> k = e.get(); if (k == null ) { e.value = null ; } else { int h = k.threadLocalHashCode & (newLen - 1 ); while (newTab[h] != null ) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
remove方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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) { e.clear(); expungeStaleEntry(i); return ; } } }
使用ThreadLocal注意事项 我们在使用ThreadLocal时,最关心的是内存泄漏问题,但经过我们以上详尽的分析,发现无论是get()、set()还是remove()方法,key为null的Entry都会被清除,那么其实Entry内部的value也就没有强引用了,则会被GC回收。
但如果没有调用过get()或者set(),就有可能 有内存泄漏的问题,所以最好的习惯是:
手动调用remove()方法,清除不再使用的ThreadLocal;
尽量将ThreadLocal设置为private static的,这样ThreadLocal会跟着线程本身一起消亡。