ThreadLocal开发中常用,通过ThreadLocal操作数据实现线程之间的隔离,保证线程之间不会因为操作同一数据导致数据安全问题。但是这种隔离是有适用范围的,也就是说在某些特定的情况下还是会出现数据安全问题的。这种特定情况下就是使用到线程池,并且在ThreadLocal使用前后没有做数据清理,就会导致安全问题,下面来看看出现的情况和具体怎么去解决。
1. ThreadLocal正常使用
一个main方法的主线程,再创建一个新的线程作为模拟线程,同时操作ThreadLocal,通过在打印对应的输出值来看ThreadLocal的作用。
public class ThreadLocalDemo {
private static ThreadLocal<Integer> local = new ThreadLocal<>();
public static void main(String[] args) {
local.set(1);
System.out.println(Thread.currentThread().getName() + "===>" + local.get());
operateLocal();
//模拟另一个线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "===>" + local.get());
local.set(4);
System.out.println(Thread.currentThread().getName() + "===>" + local.get());
operateLocal();
}).start();
}
private static void operateLocal() {
Integer value = local.get();
local.set(++value);
System.out.println(Thread.currentThread().getName() + "===>" + local.get());
}
}
输出的结果:
main===>1
main===>2
Thread-0===>null
Thread-0===>4
Thread-0===>5
从结果就可以知道,两个线程之间没有互相的干扰,各自操作各自空间里面的数据,达到了线程安全的特性。这个也是正常使用所用到的。单对于这种方式来说是线程安全的,数据也是安全的,但是下面看看会出现问题的场景。
2. 线程池下的ThreadLocal
这里创建一个线程池,为了模拟方便,线程池里面只创建一个线程,然后通过ExecutorService
两次来对ThreadLocal进行操作,你会发现一个问题。
public class ThreadLocalDemo {
private static ThreadLocal<Integer> local = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();//线程池
executorService.execute(() -> {//1
local.set(123);
System.out.println(Thread.currentThread().getName() + "==>" + local.get());
});
executorService.execute(() ->//2
System.out.println(Thread.currentThread().getName() + "==>" + local.get())
);
executorService.shutdown();
}
}
在代码块1向ThreadLocal里面添加值为123,然后打印出来对应的线程名称和ThreadLocal里面存储的值。然后在代码块2里面从ThreadLocal里面获取值。输出的结果如下:
pool-1-thread-1==>123
pool-1-thread-1==>123
两个位置输出的内容相同,也就意味着当使用线程池的时候,一个线程设置的数据可能残留在ThreadLocal里面,等下一个线程使用的时候可能直接拿到之前操作残留的数据,导致数据的污染问题。
在这里需要了解的是,在很多博客里面(当然包含我转载的一些博客)都说ThreadLocal里面的ThreadLocalMap的键是弱连接,当下一次GC回收的时候会自动回收,不会出现数据的安全问题。但是这种说法是有特定场景的,也就是说当前线程执行结束后,ThreadLocal对象需要被手动的置为null。因为GC不是时刻去执行的,是需要达到一定的条件,所以存在滞后。
在使用ThreadLocal的时候为了线程安全,基本都是放在成员变量里面,每次执行结束置为null的操作稍微不注意就不会去做,或者漏掉,这样带来的另一个麻烦就是每次使用的时候还要new创建一个ThreadLocal,总感觉很别扭。
总结一下:
- GC执行需要条件不可能在当前线程执行结束立即执行,清理掉ThreadLocal里面存储的数据
- 即使GC触发,在ThreadLocal被外部的成员变量建立了强连接,是一样清理不掉的
- 每次执行结束将null赋值给成员变量,在执行开始的时候,手动初始化ThreadLocal
3. 提出的解决方法
3.1 线程池和ThreadLocal不同时用
这个好像有点扯,线程池在正常的一个java项目中都会被用到,换句话说就是放弃ThreadLocal不用啦。所以这种解决方案过于极端。
3.2 清理ThreadLocal数据
这个是我个人目前想到的最好的方法了,可以使用面向切面的思想,切入到使用ThreadLocal的方法,在方法执行前、执行结束后、抛出异常时对ThreadLocal进行数据清理动作。
示例代码:
public static void main(String[] args) {
local.remove();//清理
try {
//……
} catch (Exception e) {
//……
} finally {
local.remove();//清理
}
}
只是demo示例,没有具体的业务内容,也没有根据切面来具体实现,偷懒了一把,只提供思路。
4. 总结
在使用ThreadLocal的时候还是要很注意的,虽然在ThreadLocal里面已经用弱连接这种机制和GC对弱连接的回收方式来实现及时的数据回收,在实际的开发中,还是存在一些问题的,所以不能完全依靠于ThreadLocal,还是要多方面的思考,做到严谨性。这个问题之前我也没有想到,在业务代码中使用到,结果是同事看到我的代码提醒我的(论有一群好的战友的重要性)。