ThreadLocal遇到线程池出现数据问题和解决方案


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,还是要多方面的思考,做到严谨性。这个问题之前我也没有想到,在业务代码中使用到,结果是同事看到我的代码提醒我的(论有一群好的战友的重要性)。


文章作者: 程序猿洞晓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 程序猿洞晓 !
评论
 上一篇
nginx基础学习(一):linux环境下nginx的安装和配置文件的初步认识 nginx基础学习(一):linux环境下nginx的安装和配置文件的初步认识
nginx目前在市场上使用是非常广泛的,作为一个开发人员,可以不会nginx的高级使用,但是基本的使用场景以及日常问题排查技能还是要有的。nginx使用的方向主要有三个,分别是路由功能、负载均衡功能以及静态资源服务功能。路由功能主要是用来将不同请求分发到不用的服务上,负载均衡是将请求分发到一个服务的集群中的不同虚拟机上,这两种……
2018-12-09
下一篇 
Spring Data JPA使用必备(二):Spring Data JPA方法命名规则实现SQL自动生成 Spring Data JPA使用必备(二):Spring Data JPA方法命名规则实现SQL自动生成
Spring data JPA是一个好东西,但是对于很多习惯于写SQL,直接怼数据库的人来说,这个真的用不习惯,还被一致认为是一个不易于程序员发展的技术。因为JPA提供了标准的封装,在操作数据库的时候,不需要写SQL,完全通过操作对象即可完成。久而久之,SQL就会被慢慢的遗忘,生疏,等以后面试的时候,也许这就是上升的一个短板。现在……
  目录