1.简介

ThreadLocal用于存储线程的局部变量,通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。ThreadLocal诞生于JDK 1.2,直到JDK5.0开始支持范型。

2.应用场景

ThreadLocal的应用场景比较广泛,例如web项目中登陆信息的存储、IOC中Request作用域的实现、Spring事物管理的实现、线程同步工具Exchanger的实现等。除了登录信息的存储以外,其他的多多少少都是涉及到底层的源码,写出来篇幅太大,所以下面简单讲述一下如何使用ThreadLocal存储登陆信息。

一般浏览器发送http请求到后端服务器,都会将用户信息的token以cookie或header形式携带过去,并在后端拦截器中对token校验是否合法。如果请求的接口中需要用到一次或多次用户信息(比如ID、名称、生日、职级等)进行业务处理,这就需要将用户信息从controller一层一层作为参数传递下去,这无疑增加了代码的复杂程度,重点是不够优雅,使用ThreadLocal完全可以避免这个问题。

1.用户信息对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
@ToString
public class UserInfo {

private Long id;

private Long deptId;

private String name;

private String phone;

private String email;

private String birthday;
}

2.创建一个ThreadLocal封装类,内部定义一个私有ThreadLocal并对外提供get、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
public class UserInfoThreadLocal {

private static final ThreadLocal<UserInfo> threadLocal = new ThreadLocal<>();

public static void set(UserInfo value){
threadLocal.set(value);
}

public static UserInfo get(){
return threadLocal.get();
}

public static void remove(){
threadLocal.remove();
}

public static UserInfo getAndValidate(){

UserInfo userInfo = get();
if(userInfo == null){
// 抛业务异常
}

return userInfo;
}
}

3.拦截器负责校验信息,并将合法的用户信息注册到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
@Component
public class LoginInfoInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

String token = request.getHeader("token");
if(StringUtils.isEmpty(token)){
// 抛业务异常
}

// 模拟查询用户信息
UserInfo userInfo = new UserInfo();
userInfo.setId(323L);
userInfo.setDeptId(542L);
userInfo.setPhone("110");
userInfo.setEmail("110@163.com");
userInfo.setBirthday("1995-12-20");

// 放入UserInfoThreadLocal
UserInfoThreadLocal.set(userInfo);

return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
UserInfoThreadLocal.remove();
}
}

4.web项目的controller层,获取并打印拦截器查询并校验通过的用户信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class TestController {

@Autowired
private TestService testService;

@RequestMapping("/test")
public void test(){

// 获取用户信息并打印
UserInfo userInfo = UserInfoThreadLocal.getAndValidate();
System.out.println("controller:" + JSONObject.toJSONString(userInfo));

// 调用service
testService.testQuery();
}
}

5.web项目的service层,获取并打印拦截器查询并校验通过的用户信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class TestServiceImpl implements TestService {

@Override
public void testQuery() {

// 获取用户信息并打印
UserInfo userInfo = UserInfoThreadLocal.getAndValidate();
System.out.println("testQuery:" + JSONObject.toJSONString(userInfo));

// 调用私有方法
testMethod();
}

private void testMethod(){

// 获取用户信息并打印
UserInfo userInfo = UserInfoThreadLocal.getAndValidate();
System.out.println("testMethod:" + JSONObject.toJSONString(userInfo));
}
}

打印结果:
controller:{“birthday”:”1995-12-20”,”deptId”:542,”email”:”110@163.com“,”id”:323,”phone”:”110”}
testQuery:{“birthday”:”1995-12-20”,”deptId”:542,”email”:”110@163.com“,”id”:323,”phone”:”110”}
testMethod:{“birthday”:”1995-12-20”,”deptId”:542,”email”:”110@163.com“,”id”:323,”phone”:”110”}

从上面的Demo代码可以看出来,只要在拦截器层面对token的验证通过,并将用户信息存储在创建的ThreadLocal对象中,就可以在任何逻辑层、任何方法直接获取用户信息,提高了代码的简洁程度。

3.存储原理

Thread类中有个成员变量threadLocals,这个变量的引用类型是ThreadLocal中的一个内部类ThreadLocalMap,这个类没有实现Map接口,本质上是一个table数组,数组中每个元素都是K-V键值对组成的Entry对象,其中K就是ThreadLocal实例,V就是要存储的局部变量对象。
图片

4.源码解析

4.1 hash算法

ThreadLocalMap的存储逻辑和HashMap有一些相似的地方,内部都是维护一个Entry类型数组,然后通过对key的哈希码进行位与运算,定位出存储的数组坐标。ThreadLocal作为key提供的哈希码查询方法并非hashCode(),而是通过成员变量threadLocalHashCode去表达。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ThreadLocal<T> {
// 哈希码
private final int threadLocalHashCode = nextHashCode();

// 下一个哈希码
private static AtomicInteger nextHashCode =
new AtomicInteger();

// 哈希码递增跨度值
private static final int HASH_INCREMENT = 0x61c88647;

// 哈希码递增方法
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}

存储哈希码的成员变量threadLocalHashCode被final修饰,也就意味着实例被创建时内部threadLocalHashCode固定为nextHashCode()方法返回的值。这个方法很有意思,每次调用都是将nextHashCode递增0x61c88647,并返回递增后的值。按照这个设计逻辑,每次创建的ThreadLocal实例的哈希值都是不同的,都会比上一次的哈希值高0x61c88647,并且考虑到会被多个线程创建,使用AtomicInteger维护递增值确保线程安全。

因此线程的threadLocals值必须由同一个实例进行存取,这样才能定位到同一个数组下坐标,这也是上述的例子中把ThreadLocal设计成static、final的原因。

4.2 set()源码

ThreadLocal的set方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void set(T value) {
// 获取调用set方法的当前线程
Thread t = Thread.currentThread();

// 获取当前线程的threadLocals属性
ThreadLocalMap map = getMap(t);

// 如果已经初始化,继续往里面增加键值对
if (map != null)
map.set(this, value);
// 如果没有初始化,创建一个ThreadLocalMap并增加一个键值对
else
createMap(t, value);
}

如果ThreadLocalMap为null,调用createMap()初始化:

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

// 为调用set方法的当前线程初始化threadLocals属性
void createMap(Thread t, T firstValue) {
// 使用带参数构造器
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

// ThreadLocalMap重载构造器
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {

// 初始化table数组容量为16
table = new Entry[INITIAL_CAPACITY];

// 对key的hashCode进行位与运算取余数,计算出存储到table数组的坐标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

// 将Entry插入ThreadLocalMap中,Entry构造器没啥复杂逻辑,不写了
table[i] = new Entry(firstKey, firstValue);

// 此时长度固定为1,因为就一个Entry
size = 1;

// 这个方法是设置ThreadLocalMap扩容的阈值(固定为容量的2/3),类似HashMap的负载因子
setThreshold(INITIAL_CAPACITY);
}

如果Map已经被初始化,调用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
private void set(ThreadLocal<?> key, Object value) {

// 获取Entry类型数组
Entry[] tab = table;

// 获取数组长度
int len = tab.length;

// 对哈希值使用位与运算定位到存储的数组坐标计作i
int i = key.threadLocalHashCode & (len-1);

// 将i坐标在数据中的值取出,赋值给e
// 即使ThreadLocal实例的哈希值不同,位与运算后仍然会计算出相同的数组坐标,只要计算出的坐标已存储元素,for循环就继续下去
// 每次循环完将i值重新赋值为i+1(如果+1后下标越界则赋值0),并将新赋值的i坐标对应的数组元素赋值给e

/**
* 1.每次循环取出数组坐标i对应的值,赋值给e
* 2.虽然每个ThreadLocal实例的哈希值不同,但是经过位于运算后仍然会冲突,并且数组中每个坐标只会存储一个对象,
* 不会像HashMap那样相同坐标使用链表或红黑树存储。
* 3.如果通过i取出的Entry为空,则跳出for循环
* 4.如果通过i取出的Entry不为空,进入循环体的逻辑处理,并且在处理完后对i+1操作(如果+1之后大于等于数组长度则设置为0)
*/
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {

// 执行到这里,说明数组下坐标i对应的Entry不为空,将Entry的key提取出来
ThreadLocal<?> k = e.get();

// 如果提取出来的key与需要set的key地址一致,直接覆盖其value
if (k == key) {
e.value = value;
return;
}

// 如果实体类Entry不为null,所属的key不为null(下面统一称为过期元素)
if (k == null) {

// k被回收,所在的e也没有存在的意义了,将需要设置的key、value覆盖到e中,覆盖完返回
replaceStaleEntry(key, value, i);
return;
}
}

// 到这里说明i的值已经递增到对应下坐标为null了,直接new一个Entry存储进去
tab[i] = new Entry(key, value);

// 数组长度+1
int sz = ++size;

// 如果没有找到过期元素,并且新增后数组的长度超过阈值,进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

如果Entry是过期元素,那么这个Entry也没有任何存在的意义,因为没有任何途径能拿到此Entry的value。这时候就需要调用replaceStaleEntry()方法将此Entry替换掉,将此数组坐标重新利用起来:

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

Entry[] tab = table;
int len = tab.length;
Entry e;

// staleSlot是前面set方法中检查出来的过期元素所在的坐标
int slotToExpunge = staleSlot;

// 对数组从slotToExpunge坐标向前遍历,直到碰到过期元素后终止
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))

/*
* 如果此Entry是过期元素,则将当前坐标赋值给slotToExpunge
* 可以理解为循环结束后,slotToExpunge的值会变为:坐标staleSlot往左最左边的过期元素
* 这个变量的作用是定位一个范围,清理过期元素工作就是将当前值作为数组坐标向右检查
*/
if (e.get() == null)
slotToExpunge = i;

// 这个循环是向后遍历,与上面的循环正好相反
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {

// 获取数组中Entry的key
ThreadLocal<?> k = e.get();

// 如果之前存在相同的key
if (k == key) {

// 覆盖原value值
e.value = value;

// staleSlot是前面set方法查询到的过期元素,与当前循环的坐标i交换位置
tab[i] = tab[staleSlot];
tab[staleSlot] = e;

// 这俩值相等说明上一个for循环,往左没有找到任何E过期元素
// 到目前为止需要清理的过期元素只有set方法检查出来的那一个,当前坐标的key和value都有值
// 然而上面俩行代码已经将当前坐标的Entry与set检查出来的Entry交换位置了
// 所以当前坐标对应的Entry是过期数据,将当前坐标赋值给slotToExpunge,准备后续清理工作
if (slotToExpunge == staleSlot)
slotToExpunge = i;

// 清理过期的数据
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

// key的位置已经定位并覆盖value了,所有逻辑处理到此结束
return;
}

// 到这里说明要设置的key在数组中还没找到,并且左边的循环查询过期元素
// 换句话说就是当前坐标i往左的元素都没毛病,往右检查清理元素的起点坐标要从i开始
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}

// 到这里说明数组遍历完了也没存在key,重新创建一个Entry并覆盖当前坐标
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);

// 如果这俩值相等,说明staleSlot坐标的左右元素都没问题,而且当前坐标也被新的Entry覆盖了,不需要清理
// 如果不相等,要么坐标staleSlot的左边存在要清理的元素,要么就是右边,执行清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

上面对数组中Entry的检查中不难发现,无论往左还是往右一旦碰到为null的元素检查就停止了,这会漏掉一部分Entry的检查。因此在每次清理的时候,顺带将不为空的元素左移,挤出所有值为null的下坐标,确保所有不为null的Entry连续挨在一起。这些逻辑都在expungeStaleEntry方法中实现:

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

//将上个方法检查出来的过期元素以及所属value的引用设置为null,方便GC回收
tab[staleSlot].value = null;
tab[staleSlot] = null;

// 数组长度减1
size--;

// 顺着坐标向右遍历
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {

// 取出当前坐标的key
ThreadLocal<?> k = e.get();

// 如果key为空,则将Entry和内部value设置为null,方便GC回收,数组长度也要减1
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 如果不为空,重新通过哈希值计算安插的数组坐标
int h = k.threadLocalHashCode & (len - 1);

// 如果通过哈希值计算的坐标不是当前坐标
if (h != i) {

// 将当前坐标清空,对应的Entry已经被e指向,并不会丢掉
tab[i] = null;

// 从哈希值计算的坐标开始,往右寻找null的坐标安放e,一旦发现要安插的坐标h左边有为null的坐标,就填充过去
while (tab[h] != null)
h = nextIndex(h, len);

// 将e赋值到最终计算出的坐标
tab[h] = e;
}
}
}

// 返回值i代表从staleSlot往右循环过程中,碰到的第一个为null的坐标
return i;
}

cleanSomeSlots主要用于控制扫描,检查还存不存在有问题的元素,如果有就清理掉。扫描的趟数为log2(数组长度),执行过程中一旦发现了过期元素,扫描趟数会重置,并且返回值变为true:

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
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;

do {

// 获取i的下一个坐标
i = nextIndex(i, len);
// 取出Entry
Entry e = tab[i];

// 如果循环过程中发现了过期元素
if (e != null && e.get() == null) {

// 重置n的值,也就是需要再次扫描log2(n)趟
n = len;

// 返回值removed设置为true
removed = true;

// 清理元素并记录清理的坐标
i = expungeStaleEntry(i);
}

// n每右移一次相当于n除以2,直到n除以2后小于0终止循环
} while ( (n >>>= 1) != 0);

// 如果为true,代表执行过程中出现需要清理的元素
return removed;
}

数组扩容:

1
2
3
4
5
6
7
8
9
private void rehash() {

// 判断是否需要扩容前,再次清理一遍过期元素
expungeStaleEntries();

// 超过阈值的3/4,进行扩容
if (size >= threshold - threshold / 4)
resize();
}
整个set方法的执行过程分为俩部分,一部分是通过哈希值与数组长度的取余运算,定位到要存储的坐标,并解决坐标冲突的问题。另一部分也是最复杂的一部分,主要涉及到对过期元素的清理工作并防止内存泄漏,因为Entry对象中key的引用是弱引用,所指向的实例如果没有其他强引用指向,随时可能被回收掉。
  • 通过实例的哈希值(threadLocalHashCode属性),定位到要存储的坐标。
  • 计算出的坐标可能会冲突(类似hashMap的哈希碰撞),这时需要对比已存储的Entry的key与要存储的key是否一致。
  • 如果key地址相同,直接覆盖value值,结束。
  • 如果key为null(过期元素),将要set的值包装成Entry覆盖当前坐标,然后进行扫描并清除所有过期元素
  • 如果计算出来的坐标不存在冲突,直接插入此坐标
  • 如果达到扩容的阈值,对数组进行扩容

4.3 get()源码

ThreadLocal的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
public T get() {

// 获取调用此方法的线程
Thread t = Thread.currentThread();

// 获取线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);

// 如果map不为null
if (map != null) {

// 获取数组元素Entry
ThreadLocalMap.Entry e = map.getEntry(this);

// 如果不为空,根据范型强转并返回,get方法到此结束
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}

// 到这里说明map为null,或者获取的Entry为null,初始化
return setInitialValue();
}

ThreadLocalMap的getEntry方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
private Entry getEntry(ThreadLocal<?> key) {

// 通过hashcode定位数组坐标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];

// 如果坐标的Entry所属的key正是要提取的key,直接返回
if (e != null && e.get() == key)
return e;
else
// 顺着坐标往右寻找
return getEntryAfterMiss(key, i, e);
}

如果通过哈希值寻找Entry失败,也就是出现了坐标冲突,那就要往后遍历寻找:

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
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

// 只要e不为null,就循环下去
while (e != null) {
ThreadLocal<?> k = e.get();

// 地址吻合说明当前Entry就是要找的对象,直接返回
if (k == key)
return e;

// 如果发现过期元素,执行清除方法
if (k == null)
expungeStaleEntry(i);
else
// 记录遍历坐标的i+1
i = nextIndex(i, len);
// 下一个Entry赋值给e
e = tab[i];
}

// 到这里说明整个数组就不存在这个key
return null;
}

setInitialValue方法没啥逻辑,就是初始化线程的threadLocals:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private T setInitialValue() {

// 这个方法写死的,返回null
T value = initialValue();

//获取当前线程
Thread t = Thread.currentThread();

// 获取当前线程的threadLocals
ThreadLocalMap map = getMap(t);

// 通过get方法进入的,map肯定为null
if (map != null)
map.set(this, value);
else
// 初始化threadLocals,并插入一个value为null的Entry
createMap(t, value);
return value;
}
get方法相对于set方法而言要简单很多,无非就是根据哈希值计算数组坐标,顺带也会清理过期元素。

4.4 remove()源码

ThreadLocal的remove()方法:

1
2
3
4
5
6
7
8
public void remove() {

// 获取当前线程的map
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果猫已经初始化,调用map的remove方法
if (m != null)
m.remove(this);
}

ThreadLocalMap的remove方法:

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
private void remove(ThreadLocal<?> key) {

// 获取数组信息
Entry[] tab = table;
int len = tab.length;

// 通过哈希值计算数组坐标赋值给i
int i = key.threadLocalHashCode & (len-1);

// 顺着坐标i往右遍历
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {

// 如果发现过期元素
if (e.get() == key) {

// 对当前Entry进行清理,方便GC回收
e.clear();

// 这个方法上面已经讲过了,向右扫描并清理过期元素
expungeStaleEntry(i);
return;
}
}
}
remove方法的作用不仅仅是清除指定的key,还会调用expungeStaleEntry()方法将清除的坐标继续向右遍历,清除遇到的所有过期元素,并且将null的坐标剔除掉。

5.内存泄漏

使用ThreadLocal可能出现的内存泄漏,是指value使用完毕后并没有被及时的清理掉。通俗点讲,我们希望线程执行完某个步骤后(value后续不会被使用)、生命周期结束前;或者线程执行完某个步骤后、归还线程池前,或者线程生命周期结束后;就该被清除但没被清除,都可以定义为内存泄漏

从乐观的角度看,ThreadLocal导致的内存泄漏并不一定导致内存溢出,也许value只是在内存中多逗留了一会。

5.1 弱引用

每个线程内部会维护一个ThreadLocalMap,用于存储当前线程的私有变量,ThreadLocalMap内部所有Entry都是以ThreadLocal对象作为key,并且Entry的key对ThreadLocal是弱引用,但是对value是强引用。如图:
图片

解决内存泄漏最直接有效的办法,就是保持使用完毕后调用remove()方法的习惯,但ThreadLocal的设计者为了避免开发者忘记调用remove()方法,提供了弱引用机制尽可能的避免内存泄漏。之所以这么说,是因为弱引用并不能百分百的避免内存泄漏,只能解决部分特定场景。

从上面的源码可以看出,每次调用get()、set()方法时,会去检查所有key为null的Entry并清除。也就是说,即使没有调用remove()方法,对应的ThreadLocal对象变成null,线程后续用到其他ThreadLocal对象的get()、set()方法时,依然可以自动帮你清理(不用白搭)。

我们平时使用ThreadLocal对象,要么设置为static全局共享,要么每次都new一个新的;在使用线程方面,要么每次创建新的线程,要么将线程池化。组合起来就是四种场景,到底哪些场景JDK可以帮我们避免,哪些需要我们开发时注意的,得一个个单独分析。

5.2 非静态化与普通线程

ThreadLocal每次使用的时候,都是new出一个新对象,线程每次也都是创建新对象。这种场景比较少见,也比较奇葩,因为ThreadLocal设计的意图,就是让value可以在任何地方获取到,这就代表任何需要value的方法,都得将ThreadLocal作为参数传进去,为啥不直接传value呢。

另外,当线程执行完某个方法,后续不再需要value时,方法出栈后ThreadLocal对象就仅剩Entry中的key弱引用形式指向它,肯定会被回收。这时Entry中的key就变成了null,如果线程后续使用了其他ThreadLocal对象的get()、set()方法,之前的value就会被自动回收。

如果Entry的key不是弱引用,key对于ThreadLocal对象是强引用,即使出栈也不会被回收,在不调用remove()方法的前提下,也就无法尽可能提前回收value。但就算没有提前回收,线程生命周期结束,ThreadLocal与线程对象都会被回收,对应的value也就GC不可达,最终还是会被回收。

5.3 非静态化与线程池

ThreadLocal每次使用的时候,都是new出一个新对象,处理任务的线程则是从线程池中获取。由于线程不会被销毁,并且会不停的处理任务,在开发者忘记调用remove()方法的前提下,会不停的往自身的ThreadLocalMap中添加value。由于ThreadLocal每次都是new出来的,弱引用设计的存在保证能被及时回收,当线程下次执行任务并用到ThreadLocal时,能自动的将上次任务用到的value清除掉。

弱引用在此场景的作用,除了尽可能的提前回收value外,还可以避免内存溢出。假设没有弱引用,线程归还线程池时,value在ThreadLocalMap中依然存在。即使下次执行任务并调用get()、set()方法,由于上次设置的Entry的key不为null,依然无法将其清除,并且value只增不减,时间久了必定导致内存溢出。

5.4 静态化与普通线程

将ThreadLocal设置为静态变量或常量,是目前比较常见的使用方式。这意味着ThreadLocal对象被class对象强引用,永远不可能被删除,弱引用也就发挥不了任何作用,开发者如果忘记调用remove()方法,是无法进行提前回收的,只能等线程生命周期结束后被GC回收。

5.5 静态化与线程池

这种场景在实际开发中使用最多,由于ThreadLocal对象被class对象强引用,永远不可能被删除,线程也不会被销毁,这就要求开发者每次使用完必须手动调用remove()方法,否则value会永远停留在线程的ThreadLocalMap中,这就有几率造成内存溢出。

为什么说有几率而不是百分百造成内存溢出呢?比如上面提到的用户登陆信息存储,虽然springMVC使用线程池来处理http请求,但好在每次进入拦截器都会重新设置value,上次请求产生的value对象在Entry中被覆盖,仅有的一个指向它的引用也没了,自然会被回收。

总结

线程的局部变量是存储在线程自身的ThreadLocalMap中,ThreadLocalMap与HashMap都属于键值对映射结构,不过ThreadLocalMap采用数组线性探测的方式存储数据,HashMap使用数组+链表存储数据。

而ThreadLocal对象仅仅只是个方便存取数据的快捷键而已,一般都是创建静态变量并提供静态存取方法,使用起来比较方便。另外在使用完毕后尽量使用remove()方法进行清理,方便GC尽快回收。

评论