一、什么是ThreadLocal
ThreadLocal用于保存线程全局变量,以方便调用。即,当前线程独有,不与其他线程共享;可在当前线程任何地方获取到该变量。
二、ThreadLocal的使用
1、如何保存内容
创ThreadLocal
实例,并调用set
函数,保存中国
字符串,分别在当前线程和new-thread
线程获取该值。通过打印结果可以看到,虽然引用的是同个对象,但new-thread
线程获取到的值却是null
。
运行结果:
1 | main 中国 |
这是什么情况呢?
在ThreadLocal
的set
函数中,获取当前线程的ThreadLocalMap
实例,如何当前线程第一次使用ThreadLocal
,则需要创建ThreadLocalMap
实例,否则直接通过ThreadLocalMap
实例的set
函数进行保存。
2、如何获取内容
由于main
线程前面set
函数将内容保存到ThreadLocalMap
实例中,已经可以获取到中国
字符串。而在new-thread
线程中,由于是第一次使用ThreadLocalMap
,所以此时map
是null
,并调用setInitialValue
函数。
在setInitialValue
函数中,调用了initialValue
函数,该函数直接返回了null
,这就是为什么在new-thread
线程获取的值是null
。因此setInitialValue
函数主要为当前线程创建ThreadLocalMap
对象。
3、ThreadLocalMap
ThreadLocalMap
内部持有一个数组table
,用于保存Entry
元素。Entry
继承至WeakReference
,并以ThreadLcoal
实例作为key
,和保存内容 T作为value
。当发生GC时,key
就会被回收,从而导致该Entry过期。
每一个线程都持有一个ThreadLocalMap
局部变量threadLocas
,如下图所示。
3.1 ThreadLocalMap的创建
ThreadLocalMap对象的创建,也就是ThreadLocal 对象调用了自身的createMap
函数。
在ThreadLocalMap
的构造函数,创建了一个保存Entry
对象的table
数组,默认大小INITIAL_CAPACITY=16
。并通过threadLocal
的threadLocalHashCode
属性计算出Entry
对象在数组的下标,然后进行保存,并计算出阈值INITIAL_CAPACITY
的2/3=16。
threadLocalHashCode
属性在ThreaLocal
对象创建时会自动计算得出。
threadLocalHashCode
在不同的ThreadLocal
实例中是不同的,通过nextHashCode.getAndAdd
函数计算出下一个实例的threadLocalHashCode
值,而第一个ThreadLocal
的threadLocalHashCode
值则是从0开始,与下一个threadLocalHashCode
间隔HASH_INCREMENT
。
通过threadLocalHashCode & (len-1)
计算出来的数组下标,分布很均匀,减少冲突。但是呢,冲突时还是会出现,如果发生冲突,则将新增的Entry放到后侧entry=null
的地方。
三、源码分析
1、ThreadLocalMap的set函数
在上一节中,分析了ThreadLocal
实例的set
函数,最终是调用了ThreadLocalMap
实例的set
函数进行保存。
通过代码分析可知,ThreadLocalMap
的set
函数主要分为三个主要步骤:
-
计算出当前
ThreadLocal
在table
数组的位置,然后向后遍历,直到遍历到的Entry
为null
则停止,遍历到Entry
的key
与当前threadLocal
实例的相等,直接更替value; -
如果遍历到
Entry
已过期(Entry
的key
为null
),则调用replaceStaleEntry
函数进行替换。 -
在遍历结束后,未出现1和2两种情况,则直接创建新的
Entry
,保存到数组最后侧没有Entry的位置。
在第2步骤和最后都会清理过期的Entry
,这个稍后分析,先看看第2步骤,在检测到过期的Entry,会调用replaceStaleEntry
函数进行替换。
replaceStaleEntry
函数,主要分为两次遍历,以当前过期的Entry为分割线,一次向前遍历,一次向后遍历。
在向前遍历过程,如果发现有过期的Entry
,则保留其位置slotToExpunge
,直到有Entry
为null
为止。这里只是判断staleSlot
前方是否有过期的Entry
,然后方便后面进行清理。
在向后遍历过程,如果发现有key
相同的Entry
,直接与staleSlot
位置的Entry
交换value
(上图注释有问题)。如果没有碰到相同的key
,则创建新的Entry
保存到staleSlot
位置。与此同时,如果向前遍历没有发现过期Entry,而在向后遍历发现过期的ntry
,则需要更新过期位置slotToExpunge
,因为后面的清除内容是需要slotToExpunge
。
2、ThreadLocalMap清除过期Entry
在上一小节中,会通过expungeStaleEntry
函数和cleanSomeSlots
函数清理过期的Entry,它们又是如何实现呢?
expungeStaleEntry
函数清理过期Entry
过程被称为:探测式清理。函数传递进来的参数是过期的Entry
的位置,工作过程是先将该位置置为null
,然后遍历数组后侧所有位置的Entry,如果遍历到有Entry
过期,则直接置null
,否则将它移到合适的位置:hash
计算出来的位置或离该hash
位置最近的位置。
经过这么一次经历,staleSlot
位置到后侧最近entry=null
的位置就不存在过期的entry
,而每个entry
要么在原有hash
位置,要么离原有hash
位置最近。
expungeStaleEntry
函数的工作范围:
expungeStaleEntry
函数一开始会将起点,即数组第3的位置设置为null
。然后开始遍历数组后侧元素,4和5位置无论是否在它的hash
位置,在这里都保持不变。遍历到第6时,发现entry
已过期,将第6设置为null
。此时3和6位置变成白色了。
A、遍历到第7的时候,假设h != i
成立,那么第7位置的entry
将被移到第6位置,空出第7位置。
B、接着遍历到第8位置,假设h != i
不成立,则第8的entry
的位置不变。
接着继续遍历后侧元素,重复着A和B步骤,直到碰到entry为null,退出遍历。例如这里的第10位置,entry=null。
由于探测性清理,碰到entry=null
的情况就会结束。而通过cleanSomeSlots
函数进行启发式清理,碰到entry=null
不停止,而是由控制条件n决定,而在这个过程中,碰到过期entry
,n又恢复到数组长度,加大清理范围。
在启发式清理过程,如果碰到过期Entry
,会导致控制条件n
恢复到数组长度len
,从而导致循环次数增加,则往后nextIndex
次数增加,从而增加清理范围。这种方式也不一定能完整清理后面所有过期元素,例如在控制n
右移所有过程中,没有碰到过期的entry
,就结束了。
3、ThreadLocalMap的扩容机制
在第1节,调用ThreadLocalMap
的set
函数最后,会调用reHash
函数进行扩容。
在外层进行启发式清理后,如果size>threshold
则会进行rehash,而在rehash
中,会清理整个数组的过期Entry
,如果清理后,数组长度还大于3/4*threshod
,则进行扩容resize
。
resize
函数直接创建新的数组,长度为旧数组的两倍。然后重新计算旧数组元素在新数组的位置,复制。
四、内存泄露
正常情况下,不再使用ThreadLocal
对象,将其将强引用设置为null
,在发生GC时,ThreadLocal
对象就会被回收。如果线程还存活(例如线程池线程的复用),就会导致Entry
的value
对象得不到释放,会造成内存泄露。所以,在使用完ThreadLocal
实例后,调用remove
函数清除一下。
疑惑
发生GC的时候,Key会被回收么,还能获取到值么?
正常情况下,ThreadLocal
实例同时被强引用所引用,所以在发生GC的时候,是不会回收的,也就是此时WeakReference.get
是有返回值的,不会被回收,直到我们设置强引用指向为null
,此时就不存在强引用,会被垃圾回收。