ThreadLocal源码分析 - Sanarous的博客

ThreadLocal源码分析

在研究其源码之前,我们需要先弄清楚两个问题。

什么是ThreadLocal

现代的软件开发过程中,并发是不可缺少的实现手段,由此带来的语言层面的切入点就是多线程,引入多线程开发后,自然要考虑多线程实现中的同步、互斥、线程安全等内容。因为这些需求就出现了以下三种来实现线程安全的手段:

  1. 互斥同步:简单点理解就是通过加锁来实现对临界资源的访问限制。加锁方式有 Synchorized 和 Lock。
  2. 非阻塞同步:前面提到的互斥同步属于一种悲观锁机制,非阻塞同步属于乐观锁机制。典型的实现方式就是 CAS 操作。
  3. 无同步方案:要保证线程安全,并不是一定就需要同步,两者没有因果关系,同步只是保证共享数据征用时正确性的手段,如果一个方法本来就不涉及共享数据,那它就不需要任何同步措施去保证正确性。ThreadLocal 的概念就是从这里引申出来的。

先通过下面这个实例来理解 ThreadLocal 的用法。先声明一个 ThreadLocal 对象,存储布尔类型的数值。然后分别在主线程中、Thread1、Thread2 中为 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
27
public class ThreadLocalDemo {
public static void main(String[] args) {

// 声明 ThreadLocal对象
ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<Boolean>();

// 在主线程、子线程1、子线程2中去设置访问它的值
mThreadLocal.set(true);

System.out.println("Main " + mThreadLocal.get());

new Thread("Thread#1"){
@Override
public void run() {
mThreadLocal.set(false);
System.out.println("Thread#1 " + mThreadLocal.get());
}
}.start();

new Thread("Thread#2"){
@Override
public void run() {
System.out.println("Thread#2 " + mThreadLocal.get());
}
}.start();
}
}

输出结果:

1
2
3
MainThread true
Thread#1 false
Thread#2 null

可以看见,在不同线程对同一个 ThreadLocal 对象设置数值,在不同的线程中取出来的值不一样。接下来就分析一下源码,看看其内部结构。

ThreadLocal源码实现

数据隔离的秘密其实是这样的,Thread 类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals ,这个变量用来保存该线程本地变量,也就是说每个线程有一个自己的 ThreadLocalMap,既然是 Map,那么就有键值对,可以简单的将它的 key 看作是 ThreadLocal 自身,而 value 就是存储的值,(实际上 key 并不是 ThreadLocal 本身,而是它的一个弱引用,下文阅读源码可以理解这么做的原因),每个线程在往某个 ThreadLocal 中塞东西的时候,实际上都是往这个线程的 ThreadLocalMap 里面存,读也是以某个 ThreadLocal 作为引用,在自己的 map 里面找对应的 key,从而实现了线程隔离。

用法

ThreadLocal 自身的 API 较少,面向程序员经常使用的方法如下:

1
2
3
4
5
static ThreadLocal<T> threadLocal = new ThreadLocal<T>() {
protected T initialValue() {
这里一般new一个对象返回
}
}

线程获取相关数据的时候只要

1
threadLocal.get();

想修改、赋值只要

1
threadLocal.set(val);

使用场景

如上面说到的,ThreadLocal 是用于线程间的数据隔离,ThreadLocal 为每个线程都提供了变量的副本。

  • 举例1:联想一下服务器(例如 Tomcat)处理请求的时候,会从线程池中取一条线程出来处理请求,如果想把每个请求的用户信息保存到一个静态变量里以便在处理请求过程中随时获取到用户信息。这时候可以建一个拦截器,请求到来时,把用户信息存到一个静态 ThreadLocal 变量中,那么在请求处理过程中可以随时从静态 ThreadLocal 变量获取用户信息。
  • 举例2:Spring 的事务实现也借助了 ThreadLocal 类。Spring 会从数据库连接池中获得一个 connection,然会把 connection 放进 ThreadLocal 中,也就和线程绑定了,事务需要提交或者回滚,只要从 ThreadLocal 中拿到 connection 进行操作。

原理分析

get() 用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //ThreadLocal是ThreadLocal的内部类
if (map != null) { //当map已存在
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); //初始化值
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

上面先取到当前线程,然后调用 getMap 方法获取对应的 ThreadLocalMap,ThreadLocalMap 是 ThreadLocal 的静态内部类,而对应的 getMap 获取的 ThreadLocalMap 对象是存储在每个 Thread 对象的 threadLocals 实例变量中的,因此可以直接获取得到。

1
ThreadLocal.ThreadLocalMap threadLocals = null;

下面来看下 ThreadLocal 的内部类 ThreadLocalMap 源码,这个静态内部类才是 ThreadLocal 的核心。

JDK1.8 中 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
static class ThreadLocalMap {
private static final int INITIAL_CAPACITY = 16; //初始数组大小,大小必须是2的幂
private Entry[] table; //每个可以拥有多个ThreadLocal,大小必须为2的幂
private int size = 0;
private int threshold; //扩容阈值

//使用了弱引用,即使Entry只能存活到下次GC之前
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

//获取某个ThreadLocal对应的值
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

//对某个ThreadLocal进行赋值
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
//循环利用key过期的Entry
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
//扩容进行rehash()
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

从整体上看,ThreadLocalMap 提供了一种为 ThreadLocal 定制的高效实现,并且自带一种弱引用的 GC 清理机制。

局部来看,既然是一个 Map(注意此处不要与 java.util.map 混淆,这里只是概念上的 map),那么肯定是有自己的 key 和 value 的,前面问题已经提及,其 key 值是 ThreadLocal 的弱引用,通过源码我们也可以看到这个结论,value 值就是实际放入的值。

那么为什么要引入弱引用呢?

其实这也说明作者 Josh Bloch 和 Doug Lea 的鬼斧神工之处,如果这里使用普通的 key-value 形式存储,实质上会造成节点的生命周期与线程强绑定,只要线程没有被销毁,那么节点在 GC 分析中一直处于可达状态,就没有办法被回收,而程序本身也无法判断是否可以清理节点。而引入弱引用就可以解决这个问题,即如果 ThreadLocal 对象没有强引用可达,那么它就活不过下次 GC,那么在 ThreadLocalMap 里面对应的 Entry 的键值就会失效,这为 ThreadLocalMap 本身的垃圾清理机制提供了便利。

我们还可以看到,ThreadLocalMap 维护了一个 Entry 表或者叫 Entry 数组,并且要求表的大小必须是 2 的幂,同时记录表里面 entry 的个数以及下一次扩容的阈值,那么为什么必须是 2 的幂呢?

带着这个问题继续往下看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//设置resize阈值以维持最坏2/3的装载因子
private void setThreshold(int len) {
threshold = len * 2 / 3;
}

//环形意义的下一个索引
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);
}

ThreadLocal 需要维持一个最坏 2/3 的负载因子,对于这个负载因子 HashMap 中早就应该接触过这个概念。

ThreadLocal 有两个方法用于得到上一个/下一个索引,注意这里实际上是环形意义下的上一个与下一个。

由于 ThreadLocalMap 使用线性探测法来解决散列冲突,所以实际上Entry[] 数组在程序逻辑上是作为一个环形存在的。 关于开放寻址、线性探测等内容,可以参考网上资料或者《计算机程序设计艺术》第三卷的 6.4 章节。

至此,我们已经可以大致勾勒出 ThreadLocalMap 的内部存储结构。下面是我绘制的示意图。虚线表示弱引用,实线表示强引用。

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

然后看一下 ThreadLocalMap 的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 构造一个包含firstKey和firstValue的map。
* ThreadLocalMap是惰性构造的,所以只有当至少要往里面放一个元素的时候才会构建它。
*/
ThreadLocalMap(java.lang.ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table数组
table = new Entry[INITIAL_CAPACITY];

// 用firstKey的threadLocalHashCode与初始大小16取模得到哈希值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

// 初始化该节点
table[i] = new Entry(firstKey, firstValue);

// 设置节点表大小为1
size = 1;

// 设定扩容阈值
setThreshold(INITIAL_CAPACITY);
}

这个构造函数在 set 和 get 的时候都可能会被间接调用以初始化线程的 ThreadLocalMap。

然后看一下上面代码中的int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 这一行,ThreadLocal 类中有一个被 final 修饰的类型为 int 的 threadLocalHashCode,它在该 ThreadLocal 被构造的时候就会生成,相当于一个 ThreadLocal 的 ID,而它的值来源于

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

private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

可以看出,它是在上一个被构造出的 ThreadLocal 的 ID / threadLocalHashCode 的基础上加上一个魔数0x61c88647的。这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为 1640531527。斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到 2654435769,如果把这个值给转为带符号的 int,则会得到 -1640531527。换句话说
(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647。通过理论与实践,当我们用 0x61c88647 作为魔数累加为每个 ThreadLocal 分配各自的 ID 也就是threadLocalHashCode再与 2 的幂取模,得到的结果分布很均匀。
ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。这就回答了上文抛出的为什么大小要为 2 的幂的问题。即为了优化效率。

对于& (INITIAL_CAPACITY - 1),相信有过算法竞赛经验或是阅读源码较多的程序员,一看就明白,对于 2 的幂作为模数取模,可以用 & (2^n - 1)来替代% 2^n,位运算比取模效率高很多。至于为什么,因为对 2^n 取模,只要不是低 n 位对结果的贡献显然都是 0,会影响结果的只能是低 n 位。

可以说在 ThreadLocalMap 中,形如key.threadLocalHashCode & (table.length - 1)(其中 key 为一个 ThreadLocal 实例)这样的代码片段实质上就是在求一个 ThreadLocal 实例的哈希值,只是在源码实现中没有将其抽为一个公用函数。

再来看看 ThreadLocalMap 的主要方法:

(1)getEntry() 方法

这个方法会被 ThreadLocal 的 get 方法直接调用,用于获取 map 中某个 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
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
private Entry getEntry(ThreadLocal<?> key) {
// 根据key这个ThreadLocal的ID来获取索引,也即哈希值
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];

// 对应的entry存在且未失效且弱引用指向的ThreadLocal就是key,则命中返回
if (e != null && e.get() == key) {
return e;
} else {
// 因为用的是线性探测,所以往后找还是有可能能够找到目标Entry的。
return getEntryAfterMiss(key, i, e);
}
}

/*
* 调用getEntry未直接命中的时候调用此方法
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

// 基于线性探测法不断向后探测直到遇到空entry。
while (e != null) {
ThreadLocal<?> k = e.get();
// 找到目标
if (k == key) {
return e;
}
if (k == null) {
// 该entry对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的entry
expungeStaleEntry(i);
} else {
// 环形意义下往后面走
i = nextIndex(i, len);
}
e = tab[i];
}
return null;
}

/**
* 这个函数是ThreadLocal中核心清理函数,它做的事情很简单:
* 就是从staleSlot开始遍历,将无效(弱引用指向对象被回收)清理,即对应entry中的value置为null,将指向这个entry的table[i]置为null,直到扫到空entry。
* 另外,在过程中还会对非空的entry作rehash。
* 可以说这个函数的作用就是从staleSlot开始清理连续段中的slot(断开强引用,rehash slot等)
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// 因为entry对应的ThreadLocal已经被回收,value设为null,显式断开强引用
tab[staleSlot].value = null;

// 显式设置该entry为null,以便垃圾回收
tab[staleSlot] = null;
size--;

Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 清理对应ThreadLocal已经被回收的entry
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
/*
* 对于还没有被回收的情况,需要做一次rehash。
*
* 如果对应的ThreadLocal的ID对len取模出来的索引h不为当前位置i,
* 则从h向后线性探测到第一个空的slot,把当前的entry给挪过去。
*/
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;

/*
* 在原代码的这里有句注释值得一提,原注释如下:
*
* Unlike Knuth 6.4 Algorithm R, we must scan until
* null because multiple entries could have been stale.
*
* 这段话提及了Knuth高德纳的著作TAOCP(《计算机程序设计艺术》)的6.4章节(散列)
* 中的R算法。R算法描述了如何从使用线性探测的散列表中删除一个元素。
* R算法维护了一个上次删除元素的index,当在非空连续段中扫到某个entry的哈希值取模后的索引
* 还没有遍历到时,会将该entry挪到index那个位置,并更新当前位置为新的index,
* 继续向后扫描直到遇到空的entry。
*
* ThreadLocalMap因为使用了弱引用,所以其实每个slot的状态有三种也即
* 有效(value未回收),无效(value已回收),空(entry==null)。
* 正是因为ThreadLocalMap的entry有三种状态,所以不能完全套高德纳原书的R算法。
*
* 因为expungeStaleEntry函数在扫描过程中还会对无效slot清理将之转为空slot,
* 如果直接套用R算法,可能会出现具有相同哈希值的entry之间断开(中间有空entry)。
*/
while (tab[h] != null) {
h = nextIndex(h, len);
}
tab[h] = e;
}
}
}
// 返回staleSlot之后第一个空的slot索引
return i;
}

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

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

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
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
private void set(ThreadLocal<?> key, Object value) {

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)]) {
ThreadLocal<?> k = e.get();
// 找到对应的entry
if (k == key) {
e.value = value;
return;
}
// 替换失效的entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) {
rehash();
}
}

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;

// 向前扫描,查找最前的一个无效slot
int slotToExpunge = staleSlot;
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,将其与无效的slot交换
if (k == key) {
// 更新对应slot的value值
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 Entry(key, value);

// 在探测过程中如果发现任何无效slot,则做一次清理(连续段清理+启发式清理)
if (slotToExpunge != staleSlot) {
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
}

/**
* 启发式地清理slot,
* i对应entry是非无效(指向的ThreadLocal没被回收,或者entry本身为空)
* n是用于控制控制扫描次数的
* 正常情况下如果log n次扫描没有发现无效slot,函数就结束了
* 但是如果发现了无效的slot,将n置为table的长度len,做一次连续段的清理
* 再从下一个空的slot开始继续扫描
*
* 这个函数有两处地方会被调用,一处是插入的时候可能会被调用,另外个是在替换无效slot的时候可能会被调用,
* 区别是前者传入的n为元素个数,后者为table的容量
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
// i在任何情况下自己都不会是一个无效slot,所以从下一个开始判断
i = nextIndex(i, len);
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();

/*
* 因为做了一次清理,所以size很可能会变小。
* ThreadLocalMap这里的实现是调低阈值来判断是否需要扩容,
* threshold默认为len*2/3,所以这里的threshold - threshold / 4相当于len/2
*/
if (size >= threshold - threshold / 4) {
resize();
}
}

/*
* 做一次全量清理
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null) {
/*
* 个人觉得这里可以取返回值,如果大于j的话取了用,这样也是可行的。
* 因为expungeStaleEntry执行过程中是把连续段内所有无效slot都清理了一遍了。
*/
expungeStaleEntry(j);
}
}
}

/**
* 扩容,因为需要保证table的容量len为2的幂,所以扩容即扩大2倍
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;

for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
} else {
// 线性探测来存放Entry
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null) {
h = nextIndex(h, newLen);
}
newTab[h] = e;
count++;
}
}
}

setThreshold(newLen);
size = count;
table = newTab;
}

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

  • 探测过程中 slot 都不无效,并且顺利找到 key 所在的 slot,直接替换即可

  • 探测过程中发现有无效 slot,调用 replaceStaleEntry,效果是最终一定会把 key 和 value 放在这个 slot,并且会尽可能清理无效 slot

    (1) 在 replaceStaleEntry 过程中,如果找到了 key,则做一个 swap 把它放到那个无效 slot 中,value 置为新值

    (2) 在 replaceStaleEntry 过程中,没有找到 key,直接在无效 slot 原地放 entry

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

(3)remove() 方法

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) {
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();
// 进行段清理
expungeStaleEntry(i);
return;
}
}
}

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

以上就是 ThreadLocalMap 的源码分析。

回顾下 ThreadLocal中 的 get() 方法中的代码

1
2
3
4
5
6
7
8
9
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();

map 为null或e为null就会走到setInitialValue,如果我们是第一次get()方法,那 map 会是空的,所以接下来先看setInitialValue()方法

1
2
3
4
5
6
7
8
9
10
11
12
private T setInitialValue() {
//调用我们实现的方法得到需要线程隔离的值
T value = initialValue();
Thread t = Thread.currentThread();
//拿到相应线程的ThreadLocalMap成员变量
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

上面 initialValue 就是实例化 ThreadLocal 要实现的方法,这里又取了线程的 ThreadLocalMap,不为空就把值 set 进去(键为 TreadLocal 本身,值就是 initialValue 返回的值);为空就创建一个 map 同时添加一个值进去,最后返回 value。

map.set(this, value)这句代码在上面的ThreadLocalMap源码中可以看到大致流程,下面看看createMap()做了什么事:

1
2
3
4
5
6
7
8
9
10
11
12
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//创建一个Entry,加入数组
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

可以看到在 new ThreadLocalMap 之后,就会创建一个 Entry 加入到数组中,最后把 ThreadLocalMap 的引用赋值给 Thread 的 threadLocals 成员变量

再回顾下 get() 方法中的代码

1
2
3
4
5
6
7
8
9
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();

现在 map 不会为空了,再次调用 get 方法就会调用 map 的getEntry()方法(上面的 ThreadLocalMap 源码中可以看到大致流程),拿到相应的 Entry,然后就可以拿到相应的值返回出去 。

分析完 get() 方法,那么 set() 方法就自然而然的明白了,就不再赘述 。

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

总结

原理

ThreadLocal 的实现原理是,在每个线程中维护一个 Map,键是 ThreadLocal 类型,值是 Object 类型。当想获取 ThreadLocal 的值时,就从当前线程中拿出 Map,然后再把 ThreadLocal 本身作为键从 Map 中拿出值返回。

优缺点

  • 优点

提供线程内的局部变量。每个线程都自己管理自己的局部变量,互不影响。

  • 缺点

可能存在的内存泄漏问题。

关于 ThreadLocal 是否会引起内存泄漏也是一个比较有争议性的问题,其实就是要看对内存泄漏的准确定义是什么。
认为 ThreadLocal 会引起内存泄漏的说法是因为如果一个 ThreadLocal 对象被回收了,我们往里面放的 value 对于【当前线程 -> 当前线程的 threadLocals (ThreadLocal.ThreadLocalMap 对象)-> Entry 数组->某个 entry.value】这样一条强引用链是可达的,因此 value 不会被回收。
认为 ThreadLocal 不会引起内存泄漏的说法是因为 ThreadLocal.ThreadLocalMap 源码实现中自带一套自我清理的机制。

之所以有关于内存泄露的讨论是因为在有线程复用如线程池的场景中,一个线程的寿命很长,大对象长期不被回收影响系统运行效率与安全。如果线程不会复用,用完即销毁了也不会有 ThreadLocal 引发内存泄露的问题。《Effective Java》一书中的第 6 条对这种内存泄露称为unintentional object retention(无意识的对象保留)。

当我们仔细读过 ThreadLocalMap 的源码,我们可以推断,如果在使用的 ThreadLocal 的过程中,显式地进行remove 是个很好的编码习惯,这样是不会引起内存泄漏。
那么如果没有显式地进行 remove 呢?只能说如果对应线程之后调用 ThreadLocal 的 get() 和 set() 方法都有很高的概率会顺便清理掉无效对象,断开 value 强引用,从而大对象被收集器回收。

但无论如何,我们应该考虑到何时调用 ThreadLocal 的 remove 方法。一个比较熟悉的场景就是对于一个请求一个线程的 server 如 Tomcat,在代码中对 web api 作一个切面,存放一些如用户名等用户信息,在连接点方法结束后,再显式调用 remove。

参考文章

  1. 深入理解 Java 之 ThreadLocal 工作原理
  2. 彻底理解ThreadLocal
  3. 【Java 并发】详解 ThreadLocal
  4. 彻底理解ThreadLocal
  5. ThreadLocal 内部实现、应用场景和内存泄漏
  6. ThreadLocal和ThreadLocalMap源码分析
如果这篇文章对您很有帮助,不妨
-------------    本文结束  感谢您的阅读    -------------
0%