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

ThreadLocal开发中常用,通过ThreadLocal操作数据实现线程之间的隔离,保证线程之间不会因为操作同一数据导致数据安全问题。但是这种隔离是有适用范围的,也就是说在某些特定的情况下还是会出现数据安全问题的。这种特定情况下就是使用到线程池,并且在ThreadLocal使用前后没有做数据清理,就会导致安全问题,下面来看看出现的情况和具体怎么去解决。

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

从结果就可以知道,两个线程之间没有互相的干扰,各自操作各自空间里面的数据,达到了线程安全的特性。这个也是正常使用所用到的。单对于这种方式来说是线程安全的,数据也是安全的,但是下面看看会出现问题的场景。

线程池下的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

提出的解决方法

线程池和ThreadLocal不同时用

这个好像有点扯,线程池在正常的一个java项目中都会被用到,换句话说就是放弃ThreadLocal不用啦。所以这种解决方案过于极端。

清理ThreadLocal数据

这个是我个人目前想到的最好的方法了,可以使用面向切面的思想,切入到使用ThreadLocal的方法,在方法执行前、执行结束后、抛出异常时对ThreadLocal进行数据清理动作。

示例代码:

public static void main(String[] args) {
    local.remove();//清理
    try {
        //……
    } catch (Exception e) {
        //……
    } finally {
        local.remove();//清理
    }
}

只是demo示例,没有具体的业务内容,也没有根据切面来具体实现,偷懒了一把,只提供思路。

总结

在使用ThreadLocal的时候还是要很注意的,虽然在ThreadLocal里面已经用弱连接这种机制和GC对弱连接的回收方式来实现及时的数据回收,在实际的开发中,还是存在一些问题的,所以不能完全依靠于ThreadLocal,还是要多方面的思考,做到严谨性。这个问题之前我也没有想到,在业务代码中使用到,结果是同事看到我的代码提醒我的(论有一群好的战友的重要性)。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿洞晓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值