ThreadLocal源码解析01
先看下set的流程图
ThreadLocal 简介 ThreadLocal 在面试中经常提到,关于ThreadLocal使用不当造成OOM以及在特殊场景下,通过ThreadLocal可以轻松实现一些看起来复杂的功能,都说明值得花时间研究其原理。
ThreadLocal 不是 Thread,是一个线程内部的数据存储类 ,通过它可以在指定的线程中存储数据,每个线程存储的是主线程变量的副本,子线程操作的是对副本进行操作,不影响其他子线程的中的数据,所以一般说,ThreadLocal不保证线程的安全,只保证线程的隔离。
举个例子,如果ThreadLocal保存保存的是一个静态变量,副本都是静态变量自己,这样就又会出现线程安全问题。
ThreadLocal 注意事项
ThreadLocal类封装了getMap()、set()、get()、remove()4个核心方法
通过getMap()获取每个子线程 Thread持有自己的ThreadLocalMap 实例, 因此它们是不存在并发竞争的。可以理解为每个线程有自己的变量副本。
ThreadLocalMap中Entry[]数组存储数据,初始化长度16,后续每次都是2倍扩容。主线程中定义了几个变量,Entry[]中就有几个key。
Entry的key是对ThreadLocal的弱引用,当抛弃掉ThreadLocal对象时,垃圾收集器会忽略这个key的引用而清理掉ThreadLocal对象, 防止了内存泄漏。
源码解读
看源码要有入口我们先从初始化开始
ThreadLocal 初始化方式 1 2 3 4 5 6 7 8 9 private static ThreadLocal<Integer> threadLocal = new ThreadLocal <Integer>(); public static void main (String[] args) { threadLocal.set(1 ); }
我们发现ThreadLocal的set方法是设置值的,他为什么能是变量的副本呢我们进入set方法
ThreadLocal.set 方法 1 2 3 4 5 6 7 8 9 10 11 12 public void set (T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) map.set(this , value); else createMap(t, value); }
我看看set方法 很简单,获取当前线程,根据当前线程获取ThreadLocalMap,有的话就set没有就创建
我们可以这样理解 ThreadLocalMap 和当前线程有关,我们进去看下
ThreadLocal.getMap方法 1 2 3 4 ThreadLocalMap getMap (Thread t) { return t.threadLocals; }
好吧到这里 确定了ThreadLocalMap 实在当前线程中存储了,这也解释了为什么是变量的副本,调用ThreadLocal的set方法实际上是将值放进当前的线程中了,每个线程中的值是不一样的。
我们需要进入Thread内部看源码了
1 2 ThreadLocal.ThreadLocalMap threadLocals = null ;
我们发现ThreadLocalMap 实在Thread类中定义的
到这里ThreadLocal.getMap方法解析完了,我们需要看createMap方法了。
ThreadLocal.createMap方法
set 方法是获取线程内部的ThreadLocalMap,初始化肯定为空,就需要调用createMap了
1 2 3 4 5 6 7 8 9 void createMap (Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap (this , firstValue); }
我看看这段代码,很有意思,ThreadLocalMap的key是threadLocal本身,value则是我们需要设置的值,这里就出现一个问题,key是相同的,如果一个ThreadLocal有多个值肯定会被覆盖,所以可以确定,ThreadLocalMap是用来处理一个线程中存在多个ThreadLocal 的问题,value肯定有更细化的对象存储,我们进去看看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 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new ThreadLocal .ThreadLocalMap.Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1 ); table[i] = new ThreadLocal .ThreadLocalMap.Entry(firstKey, firstValue); size = 1 ; setThreshold(INITIAL_CAPACITY); } private void setThreshold (int len) { threshold = len * 2 / 3 ; }
我们来看下Entry
ThreadLocalMap.Entry
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; } }
这里使用了WeakReference 软引用
WeakReference : 当一个对象仅仅被weak reference(软引用)指向, 而没有任何其他strong reference(强引用)指向的时候, 如果这时GC运行, 那么这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。
也就是如果当前线程的ThreadLocal 被销毁后,因为当前线程引用了ThreadLocalMap,所以当前线程和entry还是强引用,因为ThreadLocal在entry是软引用,所以垃圾回收key(ThreadLocal)会被销毁,entry中的value没有被销毁,但是没有key造成无法访问,这就造成了内存泄漏 ,ThreadLocal为了防止内存泄漏我们会在后面详细的说。
整体结构 我们回顾上面介绍的内容我们看下ThreadLocal整体结构的图解
ThreadLocalMap.set方法
我们上面介绍了getMap和createMap方法,我们来看看map.set方法
线性探测算法 ThreadLocalMap使用线性探测法
来解决哈希冲突,线性探测法的地址增量di = 1, 2, … , m-1,其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
先看一下线性探测相关的代码,从中也可以看出来table实际是一个环:
1 2 3 4 5 6 7 8 9 10 11 12 13 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的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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 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)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return ; } if (k == null ) { replaceStaleEntry(key, value, i); return ; } } tab[i] = new ThreadLocal .ThreadLocalMap.Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
大致分析上面都已经标注出来了,需要注意的是Entry对象是继承是WeakReference也就是一个弱引用是会被回收的,所以对应 的key值可能是为null的。存放对象之后是需要判断数组中存储对象的个数是否超过了设定的临界值threshold的大小,如果超过了需要扩容,并且还要重新计算扩容后所有对象的位置。扩容的方法是rehash()
replaceStaleEntry 替换无效的key 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 private void replaceStaleEntry (ThreadLocal<?> key, Object value, int staleSlot) { ThreadLocal.ThreadLocalMap.Entry[] tab = table; int len = tab.length; ThreadLocal.ThreadLocalMap.Entry e; int slotToExpunge = staleSlot; 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 ThreadLocal .ThreadLocalMap.Entry(key, value); if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 private int expungeStaleEntry (int staleSlot) { ThreadLocal.ThreadLocalMap.Entry[] tab = table; int len = tab.length; tab[staleSlot].value = null ; tab[staleSlot] = null ; size--; ThreadLocal.ThreadLocalMap.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; }
cleanSomeSlots 清理脏数据 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 boolean cleanSomeSlots (int i, int n) { boolean removed = false ; ThreadLocal.ThreadLocalMap.Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); ThreadLocal.ThreadLocalMap.Entry e = tab[i]; if (e != null && e.get() == null ) { n = len; removed = true ; i = expungeStaleEntry(i); } } while ( (n >>>= 1 ) != 0 ); return removed; }
n的用途
主要用于扫描控制(scan control),从while中是通过n来进行条件判断的说明n就是用来控制扫描趟数(循环次数)的 。在扫描过程中,如果没有遇到脏entry就整个扫描过程持续log2(n)次,log2(n)的得来是因为n >>>= 1
,每次n右移一位相当于n除以2。如果在扫描过程中遇到脏entry的话就会令n为当前hash表的长度(n=len
),再扫描log2(n)趟,注意此时n增加无非就是多增加了循环次数从而通过nextIndex往后搜索的范围扩大,示意图如下
rehash 重新整理
rehash 方法分两步
1、先是删除过期的对象:expungeStaleEntries();
2、如果存储对象个数大于临界值的3/4,扩容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private void rehash () { expungeStaleEntries(); if (size >= threshold - threshold / 4 ) resize(); }
expungeStaleEntries 全清理无效的entry 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private void expungeStaleEntries () { ThreadLocal.ThreadLocalMap.Entry[] tab = table; int len = tab.length; for (int j = 0 ; j < len; j++) { ThreadLocal.ThreadLocalMap.Entry e = tab[j]; if (e != null && e.get() == null ) expungeStaleEntry(j); } }
删除数组中过时的Entry对象。有些小伙伴可能会有些疑问什么是过时的Entry?为什么会过时?其实这个在前面说过,Entry是弱引用会被回收。这个方法中判断的删除条件是,Entry对象不为空并且key值为空。可见expungStaleEntry(j) 方法就是删除指定索引的Entry对象。
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 private void resize () { ThreadLocal.ThreadLocalMap.Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2 ; ThreadLocal.ThreadLocalMap.Entry[] newTab = new ThreadLocal .ThreadLocalMap.Entry[newLen]; int count = 0 ; for (int j = 0 ; j < oldLen; ++j) { ThreadLocal.ThreadLocalMap.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; }
先是创建一个是原来容量两倍的Entry[]数组,在遍历原来的数组,将key值为空的Entry对象的value置为空方便GC回收,key不为空的Entry对象先根据key的hashcode计算需要存放的位置存入新的数组中,存储结束后别忘了更新临界值。
到这里整个set方法的过程也完结了
下一篇介绍其他的方法以及threadlocal的总结