一、引言

ThreadLocal 是一个 Java 提供的线程局部变量类。它的主要作用是为每个线程提供独立的变量副本,使得每个线程都可以独立地修改自己所拥有的变量副本,而不会影响其他线程的副本

二、源码分析

1.ThreadLocalMap

ThreadLocalMap是实现ThreadLocal的根本所在,因此分析ThreadLocalMap的代码即可了解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
static class ThreadLocalMap {

/**
* 内部类Entry,使用弱引用
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

/**
* 初始化容量
*/
private static final int INITIAL_CAPACITY = 16;

/**
* Entry类型的数组,用于存储set进来的value
*/
private Entry[] table;

/**
* table大小标记
*/
private int size = 0;

/**
* 扩容阈值
*/
private int threshold; // Default to 0

/**
* 扩容阈值是长度的2/3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}

/**
* 构造器 传入第一个key,和对应的值,即第一次set值时才会进行初始化
*/
ThreadLocalMap(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);
}

/**
* 构造器 继承父线程的ThreadlocalMap
*/
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];

for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
}

2.set方法

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

这个方法十分简单,直接传进来要存储的value,然后获得ThreadLocalMap放进去即可

但是要注意,这里调用了两个其他方法,分析下这两个方法

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

这里调用了线程的threadLocals参数,这个参数如何定义的呢?在Thread类里面,如下

1
ThreadLocal.ThreadLocalMap threadLocals = null;

每个线程都有一个ThreadLocalMap类型的threadLocals变量,用于存储threadLocal值

所以set操作,最后其实都是放在了线程内,即拿到当前操作的线程,获取线程内部的threadlocals对象,将threadlocal作为key,存储进threadlocals里面去

假如线程的threadlocals对象尚未初始化,则会调用下述方法,来进行ThreadLocalMap对象的创建

1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

最终是使用ThreadLocalMap的构造器创建了对象,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 构造器 传入第一个key,和对应的值,即第一次set值时才会进行初始化
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 使用table存储value数值
table = new Entry[INITIAL_CAPACITY];
// 根据哈希取模确定位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 创建Entry对象,传入key和value
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 设置阈值
setThreshold(INITIAL_CAPACITY);
}

下面就是真正的set方法实现的地方,上面调用了map .set(this,value),最终调用的是ThreadLocalmap内的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
private void set(ThreadLocal<?> key, Object value) {

Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 发生哈希冲突,依次往后遍历,找到第一个不冲突的位置
// nextIndex保证找到数组最后之后从头开始查找
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果key相等,则覆盖
if (k == key) {
e.value = value;
return;
}
// 如果key过期或者被清理,不存在的话
// 调用replaceStaleEntry,清理失效的key和对应的value
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//
tab[i] = new Entry(key, value);
int sz = ++size;
//清理
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

这里介绍下replaceStaleEntry方法,它内部调用了cleanSomeSlots方法,而在cleanSomeSlots内部又调用了expungeStaleEntry方法,那么这三个方法有什么用呢?

首先说回threadLocalMap中数据的存储方法,是采用Entry数组进行的存储,面对冲突,采用的开放地址法解决冲突,这种方法实现简单,就是不断往后遍历,直到找到空位置,插入即可;但是也存在很大的问题,那就是当一些数据失效时,本来连续的数据就会存在空缺,这时候再进行查询,查到空位就会停止了,因此需要提供一个策略,保证失效数据被清除后能够重整数据位置,而上面的这三个方法,就是实现这个清理过程的

首先看expungeStaleEntry方法,传入的staleSlot对应位置的key为null

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
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// key为null,所以value可以直接清除
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

// Rehash 重新计算位置,调整结构
Entry e;
int i;
// 左边的都是遍历过的,无空缺,所以往后走调整就行
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// key已被清除,对应位置清除
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 比较哈希值是否相同,即判断这个key是否采用过开放地址法解决冲突
int h = k.threadLocalHashCode & (len - 1);
// 使用过开放地址法,因此
if (h != i) {
// 原位置清空
tab[i] = null;

// 还是按照开放地址法解决冲突方式,从前往后,找到空位置插入
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
// 现在的i是从传入位置,第一次往后走到的key为null位置
return i;
}

经过上面的源码分析,可以知道expungeStaleEntry方法主要是什么作用了,它主要是重新调整key为null后面那些key的位置,保证能够不出现空缺

由于在table表中,staleSlot位置的key没了,那么之前通过开放地址法经过这个key,往后排的那些key都不能再被查到了,因此需要调整它后面的数据位置,走到第一个key为null的位置即可,具体参考下图

image-20230406195704176

下面再看cleanSomeSlots方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 传入key为null的位置i,table长度n
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
// 复制一个table,可以认为局部数组访问速度加快
Entry[] tab = table;
int len = tab.length;
do {
// 往后走一步
i = nextIndex(i, len);
// 取出e
Entry e = tab[i];
// e的key失效
if (e != null && e.get() == null) {
n = len;
removed = true;
// 调用expungeStaleEntry,调整i之后的数据位置
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0); // 要遍历一遍调整位置
// 是否调整过数组,移除过无效value
return removed;
}

所以它本质上就是遍历了

一遍数组,清理并调整了数组中的对象,将无效value清除,重新调整了其他有效数据的位置

再看replaceStaleEntry方法

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
// 传入key,value和位置i,这里的key==null,要将value存储进来
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;//使用局部数组
int len = tab.length;
Entry e;

int slotToExpunge = staleSlot;
// 往前遍历,找到第一个key失效的位置slotToExpunge
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;

// 从staleSlot位置往后遍历,如果找到key相等的地方,就覆盖处理
// 没找到key相等的地方,就找到第一个为null的地方
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 覆盖
if (k == key) {
e.value = value;

tab[i] = tab[staleSlot];
tab[staleSlot] = e;

// 覆盖后,清除并调整其他元素
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}

// 现在i位置指向的是null
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}

// 放入新值
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);

// 再次清除无效key,调整其他数据
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

所以replaceStaleEntry方法就是一个把value放入table,并且清理无效key调整元素位置的方法,但是这个方法只在插入时,走到了一个key为null但是有value的地方时才会调用

3.get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public T get() {
Thread t = Thread.currentThread();
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();
}

如何查询的呢?如下

1
2
3
4
5
6
7
8
9
10
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);
}

向后查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
// 找的时候发现失效key,进行清理
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

三、总结

3.1 应用

ThreadLocal有哪些应用呢?

它的应用环境主要是线程私有一些数据的情况,如建立连接后保持连接信息—前后端,后端与数据库的连接都算,具体如下示例

  • 日期格式化工具:在多线程环境下,SimpleDateFormat 是非线程安全的,可以使用 ThreadLocal 存储每个线程的 SimpleDateFormat 实例
  • 数据库连接管理:通过 ThreadLocal 可以为每个线程分配独立的数据库连接,确保线程之间的数据库连接独立性
  • Spring 中的事务管理:Spring 使用 ThreadLocal 来管理事务,在同一个线程中的方法调用之间共享事务状态
  • 同一个线程中的业务处理需要共享数据:比如用户登录信息等

3.2 内存泄漏

首先要明白,ThreadLocal为什么把key设置成弱引用类型?

因为实际上,key-value是存储在ThreadLocalMap中的,而每个线程都有一个ThreadLocalMap类型的参数threadLocals,如果key是强引用,那么基本上除非手动指定remove,不然这个key对应的位置永远会被占用,这就造成了所说的内存泄漏问题

那么key设置成弱引用,就一定能避免内存泄漏吗?

也不一定,因为key虽然会在gc时被回收掉,但是value还存在呀,如果只是gc了,那value没被清除掉,依然会造成内存泄漏

为什么value不会被gc回收掉?

因为它是强引用类型的,实际上是存储在Entry数组上的,数组有指向它的强引用

那么如何回收,保证不会内存泄漏呢?

为了防止内存泄漏,ThreadLocal在get和set方法都能够清除无效的key对应的无效value,但是如果一直没调用这些方法,也会导致value存储在table中无法清除,造成内存泄漏

因此使用threadLocal之后,要保留调用一下remove方法的习惯

3.3 小结

由上面的分析可知,threadLocal主要依赖ThreadLocalMap来实现各种功能,数据也是包装成Entry类型存储在数组之中

而ThreadLocalMap采用的是开放地址法解决哈希冲突,若是某位置key失效了,清除后可能会对其他Entry对象的查找造成问题,所以ThreadLocalMap提供了cleanSomeSlots和expungeStaleEntry方法配合使用,可以清除key失效的无效槽,并且能够调整剩余的Entry到正确的位置