Threadlocal get() return null with async

引入

前几天的日常巡查时发现生产服务上有空指针的异常,但是业务又没有影响,然后通过查看对应的堆栈异常信息,总算是定位到了问题,但是这个问题又有点奇怪。
ThreadLocalget() 返回了空指针,但是是在当前的线程的运行中去取值,这个怎么会有问题呢?

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ThreadUtil {
ThreadLocal<Object> locals = new ThreadLocal<>();
public static Object get() {
return locals.get();
}
public static Object set(Object obj) {
return locals.set(obj);
}
}

class Service {
public void use() {
do();
}
@Async
private void do() {
ThreadUtil.get().doSomething(); // NullPointerException
}
}

分析

首先我们根据结果导向性原则进行判断这个问题,其次 ThreadLocal 通过将当前线程作为 key 存储来存储对应当前线程的共享信息,接着再去查看一下对应的源码逻辑,最终就可以得出在原有的线程被回收后,所对应的 value 没有再被引用,从而导致返回 null

分析到这里,可能一般的问题都会被解决了,可是你仔细去看看伪代码,就会发现,还存在一个特殊的注解 @Async 。而这个注解的意思是新建一个线程,然后用新线程执行调用这个方法,也就是异步调用。

我想这个问题其实已经很显而易见了,出现空指针的根本原因在于,主线程在调用异步方法后,并没有传递主线程上使用的 Object 对象给新线程,从而导致新线程在去获取对应的 Object 对象时,因为所属的线程并不相同,自而然无法从 ThreadLocal 中去拿到主线程共享的对象了。

如果对 ThreadLocal 不是很了解的话,还是首先建议先了解下。


解决办法

1、 对象传递

1
2
3
4
5
6
7
8
9
class Service {
public void use() {
do(ThreadUtil.get());
}
@Async
private void do(Object obj) {
obj.doSomething();
}
}

这种方法适合于主线程共享的对象而新线程也需要使用,主要适用于 Object 存储公共信息的情景。

2、 新建对象

1
2
3
4
5
6
7
8
9
10
class Service {
public void use() {
do();
}
@Async
private void do() {
ThreadUtil.set(this.getClass(), new Object());
ThreadUtil.get().doSomething();
}
}

而这种方法适合于主线程共享的对象对于新线程可有可无的情景,新线程新建一个也不影响原有的业务流程,一般适用于日志标记等。


引用


个人备注

此博客内容均为作者学习所做笔记,侵删!
若转作其他用途,请注明来源!