ThreadLocal的作用是提供线程内的局部变量,说白了,就是在各线程内部创建一个变量的副本,相比于使用各种锁机制访问变量,ThreadLocal的思想就是用空间换时间,使各线程都能访问属于自己这一份的变量副本,变量值不互相干扰,减少同一个线程内的多个函数或者组件之间一些公共变量传递的复杂度。我们看看源码对于ThreadLocal的描述.
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).
2. 基本用法
实现的功能是给每个线程都有自己唯一的id,且是自增的.
3. ThreadLocal的数据结构
|
|
从上面可以看出,每创建一个ThreadLocal变量,hashcode就会增加0x61c88647.hashcode的作用就是在后面根据在map中根据hash比较ThreadLocalMap的key,从而判定是否相等.之所以用这个数是因为可以是2的幂尽可能分布均匀
在每个线程内部,都会维护一个 ThreadLocal.ThreadLocalMap threadLocals
的成员变量,参考下面这个实例图.每个变量能够将变量私有化的根本原因还是在于ThreadLocalMap.
如图所示,实线是强引用,虚线是弱引用,如果ThreadLocalRef的引用没有了,则只剩下Entry对ThreadLocal有弱引用,我们知道弱引用活不过下次Gc(Entry是弱引用)
4. get()返回存储在ThreadLocalMap中value
|
|
从ThreadLocal中获取值的步骤分为如下几步.
- 获取当前线程的ThreadLocalMap
- 把当前的ThreadLocal对象为key,去获取值.若存在,且不为null,则返回.否则设置map,初始化
setInitialValue()
|
|
- initialValue()返回值为null,说明初始值为null
createMap()
- firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)相当于一个求余的方法,这要求INITIAL_CAPACITY为2的n次幂.经常采用这种方法来求响应的hash值对应在数组中的位置.
5. set()往ThreadLocalMap设置值
|
|
set()的逻辑如下
- 获取当前线程的ThreadLocalMap
- 如果map不为null,则把传入的值设置进去
- 否则创建新的map,createMap()和前面get()createMap()中的一样.
set(ThreadLocal<?> key, Object value)
set()方法的逻辑是:
- 找到在数组中的位置
- 遇到相等则替换,如果在这过程中遇到key为null,执行第三步
- 执行replaceStaleEntry()
- 经过2,3两步还没终止,说明遇到Entry为null,则把key,value组成Entry,放入到这个位置.
- 添加了新的元素,需要判断达没达到阈值,达到则需要再hash
replaceStaleEntry()
|
|
slotToExpunge主要用来记录从前到后key为null的位置,方便清理
- 第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。
cleanSomeSlots这个函数是以log(n)的速度去发现key为null的点.如果找到则调用expungeStaleEntry取清除和再hash,它里面就是不断的折半查找.
expungeStaleEntry(int staleSlot)
|
|
expungeStaleEntry的逻辑是:
- 先把该位置设置为null,方便GC
- 从当前位置顺着往下走,直到第一为null的Entry.在这过程中,如果遇到key为null,则把该位置的Entry设置为null,有利于GC.
- 如果key不为null,则把该元素重新hash(线性探测法)
rehash
|
|
rehash的逻辑是:
- 先尝试清除key为null的位置
- 再观察是否达到3/4的阈值,从而来扩容
扩容的逻辑是;
- 开辟一个长度是以前数组两倍的数组,重新hash,放入到新数组中.
- 这个过程中,如果遇到key为空,则把值赋值为null,方便GC
remove
1234567891011121314private 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(); //把引用设为null,方便GCexpungeStaleEntry(i); //上面已经谈到return;}}}
remove的处理逻辑是把应用设置为null,方便GC.然后在调用 expungeStaleEntry(i)去掉key为null的Entry,再hash.
5. 关于expungeStaleEntry中当key不为空,为什么要重新hash
是因为,如果不重新hash,那么后来再取寻找的时候,遇到Null就会停止搜索,这就造成原本能够找到的,现在找不到.归根结底采用了链地址法.
6. 使用ThreadLocal的最佳实践
我们发现无论是set,get还是remove方法,过程中key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,GC时就会被回收。那么怎么会存在内存泄露呢?但是以上的思路是假设你调用get或者set方法了,很多时候我们都没有调用过,所以最佳实践就是
- 1 .使用者需要手动调用remove函数,删除不再使用的ThreadLocal.
- 2 .还有尽量将ThreadLocal设置成private static的,这样ThreadLocal会尽量和线程本身一起消亡。
参考文章:
ThreadLocal源码深度剖析