1)问题:
才发现,写这篇博客之前,自己一直在以一种错误的姿势在用threadLocal
对象。
场景就是threadLocal
在项目中使用时,出现取值错误的情况。花了不少时间排查,最终还是排查到线程池上。之前一直没有问题,或许是因为并发不高。最终今天还是遇到了问题(出来混,迟早是要还的)。
不禁开始怀疑:threadLocal
遇到线程池就不好用了?
2)分析:
我们都知道threadLocal中维护了一个线程和value的映射,当前线程的threadLocal
即为key,value为引用的对象。
t.threadLocals = new ThreadLocalMap(this, firstValue);
每个线程保存一份,达到线程安全。
但是在线程复用的情况下,threadLocal
并不能保证按照预期执行,很有可能出现数据错乱。原因就是线程池中的线程在还未销毁的情况下,新的请求进来,会继续复用线程池中的线程,而这些线程在之前处理的过程中,对应的threadLocal
有可能已经有值,导致出错。代码如下:
/**
* Created by zhangshukang on 2018/7/27.
*/
public class ThreadLocalTest {
public static void main(String[] args) throws InterruptedException {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
ExecutorService pool = new ThreadPoolExecutor(
1,
1,
50000L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(1024),
new ThreadFactory() {
private AtomicInteger number = new AtomicInteger(0);
@Override
public Thread newThread(Runnable runnable) {
return new Thread(runnable, "admin" + "-" + number.getAndIncrement());
}
},
new ThreadPoolExecutor.AbortPolicy());
pool.execute(()->{
System.out.println(Thread.currentThread().getName());
threadLocal.set(5);
System.out.println(threadLocal.get());
});
Thread.sleep(3000l);
System.out.println("-------------------");
pool.execute(()->{
System.out.println(Thread.currentThread().getName());
System.out.println(threadLocal.get());
});
}
}
执行结果如下:
admin-0
5
-------------------
admin-0
5
可以看到上面的执行结果:线程 admin-0 复用,输出了相同的变量。
3)正确姿势:
正确姿势是在threadLocal
变量使用之后,调用remove()
方法。这么做也可以避免内存泄露,如果没有调用该方法,那当map中当前线程被回收,对应的value得不到回收,容易引起内存泄露。
这样又引出了新问题,就是我们怎么区分在什么地方进行remove()
呢?假如一个接口调用了remove()
,然后结束了代码逻辑,这没问题。但是有可能另一个接口也执行完该代码块remove()
,接着又在别的地方调用了get()
方法,这种场景还是会出错。
所以尽量避免这种使用场景,要保证remove()
的代码块的调用链后面,不会再执行get方法。