ThreadLocal类可以使每个线程保存一份线程局部变量,也就是当前线程持有一个变量,各个线程之间的这个变量不受影响。一个线程可以有多个ThreadLocal实例。
简介
用法如下:
1 | public class Temp { |
从上可以看出,每一个线程都共享同一个ThreadLocal,但是他们又存有一个线程局部变量,这里是一个Integer,每一个局部变量都不影响对方的存在。
成员变量
private static AtomicInteger nextHashCode = new AtomicInteger();
这是一个线程安全的Integer,表示一个ThreadLocal的hashcode。从0开始原子的增加。private final int threadLocalHashCode = nextHashCode();
表示当前的ThreadLocal的hashcode,显然每一个ThreadLocal的hashcode都不相同。private static final int HASH_INCREMENT = 0x61c88647;
表示每一次ThreadLocal都自增HASH_INCREMENT大小。
threadLocalHashCode是一个很重要的变量,ThreadLocal保存的值实际上是在一个Map中,而且key就是根据hashcode来计算这个值应该在数组的哪个位置。
在了解这个类的工作机制之前,先了解一些其他要使用的类。
Entry类
Entry类是一个数据结构类,表示一个map的节点。
它继承类弱引用,当此时的线程消失,它的key就会被垃圾回收器回收。也就是会造成内存泄露,不过在ThreadLocal中会经常检查key为null的值,然后清除掉,所以避免了内存泄露。
它的key就是当前的ThreadLocal,value就是保存的值。
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
ThreadLocalMap
ThreadLocalMap是一个真正实现存储逻辑的一个类,上面的Entry是这个类的静态内部类。
Map的数据结构是一个Entry数组实现的,而处理哈希冲突是采用线性探查法。
注意
ThreadLocalMap使用的解决哈希冲突的方法是线性探测法,并且删除一个节点的时候它真的删除了这个节点(有些方法是标记该位置已经被删除,而不是删除)。
试想一种情况,一个ThreadLocal的实例a,它在set的的时候,hashcode&(len-1)得到的位置应该10,但是从10到15都有节点,那么它被放在了16的位置,假如此时10位置(或者11,12,13等等)的位置的节点被remove掉了,下一次有重新设置a的保存的值,那么此时可能就会将它放在10位置,就存在的两个key相同节点。
ThreadLocalMap并没有这个bug,那么它的解决办法是:在remove时,它会清除该节点之后的无用节点(避免内存泄露),以及将一些节点”往前挪”(保证hash的正确性),也可能不往前挪,保证了一个优先级:一个节点本来应该放在一个数组的位置空闲了,那么这个节节点就会改变现在的位置来占有这个位置。
实际上清除无用节点以及“挪动节点”很频繁,在getEntryAfterMiss,remove,replaceStaleEntry,cleanSomeSlots,expungeStaleEntries都调用了这个方法。那么它为什么不使用链地址法来解决哈希冲突呢,我认为因该是在一个线程中,ThreadLocal不会太多,所以没必要使用链地址法,经常遍历与移动也耗费不了太多时间吧。
因此它具有以下成员变量:
成员变量
private static final int INITIAL_CAPACITY = 16;
初始容量,必须是2的幂。因为2的幂-1用来与&hanshcode可以很方便的寻找该key在数组的坐标。private Entry[] table;
哈希数组private int size = 0;
map的节点数private int threshold;
当节点的数量达到了这个数,就表示该扩容了。
成员函数
为了实现它的功能:put,remove,set,rehash等,它具有以下方法。
构造函数
传入了当前的key和value,然后new了一个长为INITIAL_CAPACITY的数组,在获取到该节点的坐标(hash&(cap-1)),然后将new一个节点放进数组1
2
3
4
5
6
7ThreadLocalMap(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);
}
getEntry
获取一个节点,先找到它应该在的位置,如果不再,就往后探测
1 | private Entry getEntry(ThreadLocal<?> key) { |
getEntryAfterMiss:用该方法获取,依次往后遍历,如果找到那么就返回该节点,如果遇到了null,那么就清除一些节点(清除节点是额外的事,相当于顺带做一些好事)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}
set(ThreadLocal<?> key, Object value)
set方法用来设置新值或更改旧值
1 | private void set(ThreadLocal<?> key, Object value) { |
void remove(ThreadLocal<?> key)
删除一个节点
1 | private void remove(ThreadLocal<?> key) { |
private int expungeStaleEntry(int staleSlot)
很重要的一个方法,用来清除从staleSlot位置之后的应该清除的节点
1 | private int expungeStaleEntry(int staleSlot) { |
private boolean cleanSomeSlots(int i, int n)
该方法尝试删除一些节点,也可能不会删除,如果删除了,那么返回true
1 | private boolean cleanSomeSlots(int i, int n) { |
resize
就是直接简单的扩容,然后重新计算每个节点应该在的位置即可。
成员函数
接下来是ThreadLocal的一些常用的函数
get()
获取当前ThreadLocal所保存的值。
1 | public T get() { |
set()
设置当前ThreadLocal对应的值。依旧是先获取ThreadLocalMap,然后增加或者设置值而已。
1 | public void set(T value) { |
其他
还有一些辅助的函数,比如创造一个map,remove节点等等,都很简答。
为什么不将ThreadLocalMap放入Thread中呢?
ThreadLocalMap不是必需品,定义在Thread中增加了成本,定义在ThreadLocal中按需创建即可。
总结
每一个Thread实例,都持有一个ThreadLocalMap实例(可能为空),当我们使用ThreadLocal保存一些数据时,实际上是向这个Map中写入数据。key是该ThreadLocal,值就是该ThreadLocal想要保存的值。