ThreadLocal 叫做本地线程变量,意思是说,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。

一、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
public class ThreadLocalTest {

static ThreadLocal<String> localVar = new ThreadLocal<>();

static void print(String str) {
//打印当前线程中本地内存中本地变量的值
System.out.println(str + " :" + localVar.get());
//清除本地内存中的本地变量
localVar.remove();
}

public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程1中本地变量的值
localVar.set("localVar1");
//调用打印方法
print("thread1");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
});

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程1中本地变量的值
localVar.set("localVar2");
//调用打印方法
print("thread2");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
});

t1.start();
t2.start();
}
}

运行结果如下

1
2
3
4
thread2 :localVar2
thread1 :localVar1
after remove : null
after remove : null

由此可见,各线程的 ThreadLocal 是互不相关的。

实际上,可以把ThreadLocal看成一个全局Map<Thread, Object>:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key:

1
Object threadLocalValue = threadLocalMap.get(Thread.currentThread());

因此,ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。

二、web项目ThreadLocal保存用户信息

项目概述:

1
2
3
4
在登录代码中,当用户登录成功时,生成一个登录凭证存储到redis中,
将凭证中的字符串保存在cookie中返回给客户端。
使用一个拦截器拦截请求,从cookie中获取凭证字符串与redis中的凭证进行匹配,获取用户信息,
将用户信息存储到ThreadLocal中,在本次请求中持有用户信息,即可在后续操作中使用到用户信息。

使用ThreadLocal替代Session完成保存用户登录信息功能,这样既可以在同一线程中很方便的获取用户信息,又不需要频繁的传递session对象。

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
@Component
public class LoginInterceptor implements HandlerInterceptor {

@Autowired
private LoginService loginService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//在执行controller方法(Handler)之前执行
/**
* 1.需要判断 请求的接口路径是否为 HandlerMethod(controller方法)
* 2.判断 token 是否为空, 如果为空:未登录
* 3.如果 token 不为空,登录验证 loginService checkToken
* 4.如果 认证成功 放行
*/
if (!(handler instanceof HandlerMethod)){//还可能是 ResourceHttpRequestHandler,用来处理静态资源请求的handler,默认去classpath下的static目录去查询
return true;
}
String token = request.getHeader("Authorization");

if (StringUtils.isBlank(token)){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
SysUser sysUser = loginService.checkToken(token);
if (sysUser == null){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
//验证成功,放行
//我希望在controller中 直接获取用户的信息 怎么获取?
UserThreadLocal.put(sysUser);
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//如果不删除 ThreadLocal中用完的信息,会有内存泄露的风险
//四种:强引用(不会被回收)、软引用(内存不足时回收)、弱引用(gc时直接回收)、虚引用(差不多不存在的引用)
UserThreadLocal.remove();
}
}

三、ThreadLocal内存泄漏

img_1.png

实线代表强引用,虚线代表弱引用

每一个Thread维护一个ThreadLocalMap

其中key为指向ThreadLocal的一个弱引用,value为线程变量的副本

如果一个ThreadLocal不存在外部强引用时(线程结束),Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key==null, 而value还存在着强引用,只有thread线程退出以后,value的强引用链条才会断掉。

重点来了,key 为什么被设计为弱引用?

参考链接