深入理解ThreadLocal

什么是ThreadLocal

ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。它采用空间来换取时间的方式,解决多线程中相同变量的访问冲突问题

官方demo

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
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);

// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};

// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}

public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
public void run() {
System.out.print(threadId.get());
}
}).start();
}
}
}

输出结果:03214

ThreadLocal实现原理

创建一个ThreadLocal实例后,每当某个线程调用ThreadLocal.get()/set()时,会去访问当前线程内部的threadLocals,threadLocals是一个ThreadLocalMap对象,内部存储了Entry数组,Entry是一个弱引用的key-value对象,key是ThreadLocal,value是当前线程存的值

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 ThreadLocal<T> {
public T get() {
...
}
public void set(T value) {
...
}
public void remove() {
...
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {

Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

private ThreadLocalMap.Entry[] table;
}
}

Thread类内部结构

1
2
3
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal及Thread之间的关系

image

由此可见,在ThreadLocal中并没有对于ThreadLocalMap的引用,ThreadLocalMap的引用是在Thread类中,因此每个线程在向ThreadLocal里设置值时,其实都是向自己所持有的ThreadLocalMap里设置值;读的时候同理

ThreadLocal源码解析与设计思路

ThreadLocalMap提供了一种为ThreadLocal定制的高效实现,并且自带一种基于弱引用的垃圾清理机制。
下面从基本结构开始一点点解读。

存储结构

说ThreadLocalMap是Map,是因为它内部的Entry是包含key和value,我们来看下ThreadLocalMap的内部结构:

1
2
3
4
5
6
7
8
9
10
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private ThreadLocalMap.Entry[] table;
}

ThreadLocalMap内部是Entry的数组,Entry中key是ThreadLocal实例,value是存放塞到ThreadLocal里的值

关于WeakReference与Java的四种引用

Java的四种引用

  • 强引用:最普遍的引用,如果一个对象具有强引用,那垃圾回收器绝不会回收它
1
2
Object o = new Object();  // 强引用
o = null; // 帮助垃圾收集器自动回收此对象
  • 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用
1
2
3
4
5
6
7
8
SoftReference<String> softRef=new SoftReference<String>("hello");  // 软引用
If(JVM.内存不足()) {
System.gc(); // 垃圾回收器进行回收
softRef.get(); // 内存不足,软引用则被回收,此处为null
}else{
System.gc(); // 垃圾回收器进行回收
softRef.get(); // 内存足够,软引用未被回收,此处为"hello"
}
  • 弱引用:弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
1
2
3
WeakReference<String> weakRef = new WeakReference<String>("hello");  // 弱引用
System.gc(); // 垃圾回收器进行回收
weakRef.get(); // 被垃圾回收器回收,此处为null
  • 虚引用:“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
1
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

关于WeakReference的使用

我们关注到ThreadLocalMap内部的Entry是继承WeakReference实现的,来看看WeakReference弱引用的用法

直接使用

1
2
3
4
5
Apple apple = new Apple("苹果");
WeakReference<Apple> appleWeakReference = new WeakReference<>(apple);
appleWeakReference.get(); // 此处不为空
System.gc(); // 垃圾回收器进行回收
appleWeakReference.get(); // 此处为null

继承使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 继承WeakReference,将Apple作为弱引用。
* 注意到时候回收的是Apple,而不是Salad
*/
public class Salad extends WeakReference<Apple> {
public Salad(Apple apple) {
super(apple);
}
}
Apple apple = new Apple("苹果");
Salad appleWeakReference = new Salad<>(apple);
appleWeakReference.get(); // 此处不为空
System.gc(); // 垃圾回收器进行回收
appleWeakReference.get(); // 此处为null

为什么使用弱引用

因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,Entry强引用ThreadLocal,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,这就造成了内存泄漏,当然也可以等到线程被回收销毁,但是在使用线程池去维护线程时,可能线程会被反复利用,导致脏数据一直存在。

这里使用弱引用,弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。

ThreadLocal是如何解决hash冲突的

首先我们来看一下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
25
26
public class ThreadLocal<T> {
/**
* ThreadLocals依赖于每个线程(Thread.threadLocals和 InheritableThreadLocals)附加的每个线程的线性探针哈希映射。
* ThreadLocal对象充当键,通过threadLocalHashCode搜索。这是一个自定义哈希码(仅在ThreadLocalMaps中有用),
* 在相同的线程使用连续构造的ThreadLocals 的情况下,消除了冲突
*/
private final int threadLocalHashCode = nextHashCode();

/**
* 下一个hashcode
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();

/**
* 生成hash code间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中
*/
private static final int HASH_INCREMENT = 0x61c88647;

/**
* 返回下一个hashcode
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}

我们注意到,每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定大小0x61c88647。nextHashCode是静态的AtomicInteger,所有ThreadLocal对象共享getAndAdd(HASH_INCREMENT)

为什么采用0x61c88647这个数字呢?

16进制 10进制 2进制
0x61c88647 1640531527 01100001110010001000011001000111
2654435769(取反后的10进制) 10011110001101110111100110111001(取反+1)

斐波那契散列法中讲到2654435769是一个斐波那契散列乘数,它的优点是通过它与2的幂取模,得到的结果分布很均匀,可以很大程度上避免hash冲突

再来看一下ThreadLocalMap的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 构造一个最初包含(firstKey,firstValue)的新Map。
* ThreadLocalMaps是惰性构造的,因此只有在至少要放置一个条目时才创建
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table数组
table = new ThreadLocalMap.Entry[INITIAL_CAPACITY];
// 用firstKey的threadLocalHashCode与初始大小16取模得到哈希值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 初始化该节点
table[i] = new ThreadLocalMap.Entry(firstKey, firstValue);
// 设置节点表大小为1
size = 1;
// 设定扩容阈值
setThreshold(INITIAL_CAPACITY);
}

我们重点关注一下threadLocalHashCode & (INITIAL_CAPACITY - 1),相信有过算法竞赛经验或是阅读源码较多的程序员,一看就明白,对于2的幂作为模数取模,可以用&(2^n-1)来替代%2^n,位运算比取模效率高很多

根据如下代码我们会发现用魔数0x61c88647,斐波那契散列取值发布得更均匀

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
public class HashTest {
private static final int size = 16;
private static final int threshold = size - 1;
private static final AtomicInteger atomic = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;

public static void main(String[] args) {
List<Integer> hashCode = new ArrayList<>();
List<Integer> fiboHash = new ArrayList<>();
List<Integer> murmurHash = new ArrayList<>();
List<Integer> consistentHash = new ArrayList<>();

for (int i = 0; i < size; i++) {
Object a = new Object();
hashCode.add(a.hashCode() & threshold);
fiboHash.add(atomic.getAndAdd(HASH_INCREMENT) & threshold);
murmurHash.add(Hashing.murmur3_32().hashInt(i).asInt() & threshold);
consistentHash.add(Hashing.consistentHash(i, size) & threshold);
}

System.out.println(hashCode.stream().sorted().map(x -> String.valueOf(x)).collect(Collectors.joining(",")));
System.out.println(fiboHash.stream().sorted().map(x -> String.valueOf(x)).collect(Collectors.joining(",")));
System.out.println(murmurHash.stream().sorted().map(x -> String.valueOf(x)).collect(Collectors.joining(",")));
System.out.println(consistentHash.stream().sorted().map(x -> String.valueOf(x)).collect(Collectors.joining(",")));
}
}

运行结果如下:

1
2
3
4
0,1,1,3,4,6,8,9,9,10,11,14,14,15,15,15
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
1,1,1,1,2,3,6,7,10,10,11,13,14,14,14,15
0,0,4,6,8,9,10,10,12,12,13,13,14,14,14,15

可以看出斐波散列分布很均匀,没有冲突。其他hashcode、murmurHash、consistentHash都会有或多或少的冲突。不过斐波散列需要AtomicInteger共享变量

是否可以直接用AtomicInteger递增取模,而不用递增0x61c88647来取模?

当插入新的Entry且出现Entry冲突,而进行线性探测时,后续的Entry坑也极大可能被占了(因为之前是连续存储),使得线性探测性能差。而斐波散列的nextIndex()很大可能是有坑且可以插入的。Netty的FastThreadLocal是AtomicInteger递增的

ThreadLocalMap使用线性探测法来解决散列冲突

ThreadLocal有两个方法用于得到上一个/下一个索引,注意这里实际上是环形意义下的上一个与下一个。由于ThreadLocalMap使用线性探测法来解决散列冲突,所以实际上Entry[]数组在程序逻辑上是作为一个环形存在的。
至此,我们已经可以大致勾勒出ThreadLocalMap的内部存储结构。下面是我绘制的示意图。虚线表示弱引用,实线表示强引用。

image

ThreadLocalMap维护了Entry环形数组,数组中元素Entry的逻辑上的key为某个ThreadLocal对象(实际上是指向该ThreadLocal对象的弱引用),value为代码中该线程往该ThreadLoacl变量实际塞入的值。

线性探测法

线性探测法就是从冲突的数组slot开始,依次往后探测空slot,如果到数组尾部,再从头开始探测(环形查找)

set()解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 将此线程局部变量的当前线程副本设置为指定值
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
/**
* 获取当前线程的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 创建当前线程的ThreadLocalMap,并设置firstValue
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
/**
* 将此线程局部变量的当前线程副本设置为指定值
*/
private void set(ThreadLocal<?> key, Object value) {
ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
// 根据ThreadLocal的散列值,查找对应元素在数组中的位置
int i = key.threadLocalHashCode & (len - 1);
// 使用线性探测法查找元素,通过环型查找,直到找到e==null的情况或覆盖
for (ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// ThreadLocal存在时,直接覆盖之前的值
if (k == key) {
e.value = value;
return;
}
// key为null,但是值不为null,说明之前的ThreadLocal对象已经被回收了,当前数组中的Entry是
if (k == null) {
// 用新元素代替陈旧的元素,并cleanSomeSlots()
replaceStaleEntry(key, value, i);
return;
}
}
// 当前e=null,则new一个新的entry
tab[i] = new ThreadLocalMap.Entry(key, value);
int sz = ++size;
// 如果数量达到域值,则扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* 用新元素代替陈旧的元素,并cleanSomeSlots()
*/
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;
// 向前扫描,查找最前的一个无效slot
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 向后遍历table
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果在向后环形查找过程中发现key相同的entry就覆盖并且和脏entry进行交换
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
/*
* 如果在整个扫描过程中(包括函数一开始的向前扫描与i之前的向后扫描)
* 找到了之前的无效slot则以那个位置作为清理的起点,
* 否则则以当前的i作为清理起点
*/
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 从slotToExpunge开始做一次连续段的清理,再做一次启发式清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果当前的slot已经无效,并且向前扫描过程中没有无效slot,则更新slotToExpunge为当前位置
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果key在table中不存在,则在原地放一个即可
tab[staleSlot].value = null;
tab[staleSlot] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
// 在探测过程中如果发现任何无效slot,则做一次清理(连续段清理+启发式清理)
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* 通过重新散列位于staleSlot和下一个null插槽之间的任何可能冲突的条目
* 来消除陈旧的条目。这还会删除在结尾的null之前遇到的所有其他过时的条目
*/
private int expungeStaleEntry(int staleSlot) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
// 清理脏entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 往后查找entry,直到null。如果遇到过时则清理,有值的重新哈希到合适位置
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;
// 找到空槽,赋予当前entry
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
/**
* 启发式地清理slot,
* i对应entry是非无效(指向的ThreadLocal没被回收,或者entry本身为空)
* n是用于控制控制扫描次数的
* 正常情况下如果log n次扫描没有发现无效slot,函数就结束了
* 但是如果发现了无效的slot,将n置为table的长度len,做一次连续段的清理
* 再从下一个空的slot开始继续扫描
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
do {
// i在任何情况下自己都不会是一个无效slot,所以从下一个开始判断
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;
}
/**
* 重新包装和/或调整桌子大小。首先扫描整个*表,删除陈旧的条目。如果这还不足以*缩小表的大小,则将表的大小加倍。
*/
private void rehash() {
expungeStaleEntries();
// 使用较低的阈值加倍以避免滞后
if (size >= threshold - threshold / 4)
resize();
}
/**
* 将表的容量加倍
*/
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; // Help the GC
} 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;
}
/**
* 清除表中所有过时的条目
*/
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);
}
}

我们来回顾一下ThreadLocal的set方法可能会有的情况

  • 探测过程中slot都不无效,并且顺利找到key所在的slot,直接替换即可
  • 探测过程中发现有无效slot,调用replaceStaleEntry,效果是最终一定会把key和value放在这个slot,并且会尽可能清理无效slot

    在replaceStaleEntry过程中,如果找到了key,则做一个swap把它放到那个无效slot中,value置为新值
    在replaceStaleEntry过程中,没有找到key,直接在无效slot原地放entry
    
  • 探测没有发现key,则在连续段末尾的后一个空位置放上entry,这也是线性探测法的一部分。放完后,做一次启发式清理,如果没清理出去key,并且当前table大小已经超过阈值了,则做一次rehash,rehash函数会调用一次全量清理slot方法也即expungeStaleEntries,如果完了之后table大小超过了threshold - threshold / 4,则进行扩容2倍

get()解析

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
/**
* 返回此线程局部变量的当前线程副本中的值。如果变量没有当前线程的值,则首先通过调用initialValu方法将其初始化为返回的值
*/
public T get() {
Thread t = Thread.currentThread();
// 首先会获取当前线程的ThreadLocalMap,如果为空则初始化一个
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();
}
/**
* 首先会获取当前线程的ThreadLocalMap,如果存在则覆盖,否则createMap
*/
private T setInitialValue() {
// 默认初始值为null
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
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 创建当前线程的ThreadLocalMap,并设置firstValue
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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 ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
// 根据key这个ThreadLocal的ID来获取索引,也即哈希值
int i = key.threadLocalHashCode & (table.length - 1);
// 对应的entry存在且未失效且弱引用指向的ThreadLocal就是key,则命中返回
ThreadLocalMap.Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
/**
* 当在直接哈希槽中找不到key时使用的getEntryAfterMiss(),内部调用expungeStaleEntry清除"脏entry"
* 脏entry的生产是由于在ThreadLocal实例再被回收之后,线程内部Entry的key是弱引用,所以被回收,而value是强引用,所以无法被回收
*/
private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocal.ThreadLocalMap.Entry e) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
// 如果key过期则清理数据,否则环形查找下一个
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

我们来回顾一下从ThreadLocal读一个值可能遇到的情况:
根据入参threadLocal的threadLocalHashCode对表容量取模得到index

  • 如果index对应的slot就是要读的threadLocal,则直接返回结果
  • 调用getEntryAfterMiss线性探测,过程中每碰到无效slot,调用expungeStaleEntry进行段清理;如果找到了key,则返回结果entry
  • 没有找到key,返回null

remove()解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 删除此线程局部变量的当前线程值
*/
public void remove() {
ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* 获取当前线程的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 从map中删除ThreadLocal
*/
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
// 显式断开弱引用
e.clear();
// 进行段清理
expungeStaleEntry(i);
return;
}
}
}

remove方法相对于getEntry和set方法比较简单,直接在table中找key,如果找到了,把弱引用断了做一次段清理。

关于内存泄露

关于ThreadLocal是否会引起内存泄漏也是一个比较有争议性的问题,其实就是要看对内存泄漏的准确定义是什么。

  • 认为ThreadLocal会引起内存泄漏的说法是因为如果一个ThreadLocal对象被回收了,我们往里面放的value对于【当前线程->当前线程的threadLocals(ThreadLocal.ThreadLocalMap对象)->Entry数组->某个entry.value】这样一条强引用链是可达的,因此value不会被回收。

  • 认为ThreadLocal不会引起内存泄漏的说法是因为ThreadLocal.ThreadLocalMap源码实现中自带一套自我清理的机制。

之所以有关于内存泄露的讨论是因为在有线程复用如线程池的场景中,一个线程的寿命很长,大对象长期不被回收影响系统运行效率与安全。如果线程不会复用,用完即销毁了也不会有ThreadLocal引发内存泄露的问题。

当我们仔细读过ThreadLocalMap的源码,我们可以推断,如果在使用的ThreadLocal的过程中,显式地进行remove是个很好的编码习惯,这样是不会引起内存泄漏。

那么如果没有显式地进行remove呢?只能说如果对应线程之后调用ThreadLocal的get和set方法都有很高的概率会顺便清理掉无效对象,断开value强引用,从而大对象被收集器回收。

总结下使用ThreadLocal时会发生内存泄漏的前提条件:

  • ThreadLocal引用被设置为null,且后面没有set,get,remove操作
  • 线程一直运行,不停止(线程池场景)
  • 触发了垃圾回收(Minor GC或Full GC)

由此可见我们可以总结以下两个原则来避免内存泄露:

  • ThreadLocal申明为 private static final

    Private与final 尽可能不让他人修改变更引用,
    Static 表示为类属性,只有在程序结束才会被回收。
    
  • ThreadLocal使用后务必调用 remove() 方法

    最简单有效的方法是使用后将其移除
    

阿里巴巴开发手册中提到

image

注意:每个线程往threadlocal中读取数据都是线程隔离,互相之间不影响,所以threadlocal无法解决共享对象的更新问题。由于不需要共享信息,自然不存在竞争问题了,从而保证了某些情况下线程安全问题,以及避免了某些情况必须要考虑线程安全必须同步带来的性能损失

总结

从ThreadLocal的内部结构入手来了解ThreadLocal的运行原理,利用斐波那契散列法结合线性探测法来做数据的存储,以及解释了ThreadLocal内存泄露的问题。

参考文献