记一个由多线程并发引发的异常


前提

这篇博文解决的是上次重构我的日报系统时遗留下的问题,关于日报系统的简要说明请参阅《记一次重构》.


任务工厂

根据需求,我需要创建一个任务工厂类,用来给定时线程构建任务:

public class TaskFactory {
    private int task;
    private Runnable runnable;
    
    public void setTask(int task){
    	this.task = task;
    }
    
    public void build(){
        runnable = () -> {
            System.out.print(String.valueOf(task));
        };
    }
    
    public Runnable getRunnable() {
        return runnable;
    }
}

其中:
task是需要执行的任务所包含的信息(此处简化为一个整形值);
runnable是所需的定时线程,用于定时执行指定的任务(此处简化为输出任务所代表的数字);
③调用逻辑即依所有成员函数的顺序各调用一次。


使用场景示例

假定一个场景中,我需要起三个线程来分别执行值为1、2、3的三个任务:

		TaskFactory taskFactory = new TaskFactory();  //定义任务工厂
		taskFactory.setTask(1);  //设置任务
        taskFactory.build();  //构建线程
        Runnable runnable1 = taskFactory.getRunnable();
        
        taskFactory.setTask(2);
        taskFactory.build();
        Runnable runnable2 = taskFactory.getRunnable();
        
        taskFactory.setTask(3);
        taskFactory.build();
        Runnable runnable3 = taskFactory.getRunnable();
        
        runnable1.run();  //执行任务
        runnable2.run();
        runnable3.run();

任务发生了覆盖

按照我们的预期,程序应该输出:

123

但是运行程序后我们会发现,输出是:

333

很明显,后面的任务覆盖了前面的任务,原因在于int task的时候虚拟机给task这个变量分配了一个地址,而print函数里面指定的也是这个地址,因此,当我们调用了三次taskFactory.setTask时,仅仅改变了这个地址里存放的值,而并没有给每个Runnable都分配一份。所以三个Runnable都执行了最后一个被设定的任务。


解决方案

这里需要引入ThreadLocal类,他通过给每一个线程分配一份指定变量的副本,保证并发情况下的线程安全。 通俗一些来说,它可以让每一个线程都维护一个属于自己的指定变量,因而各线程间不会因为共享变量而导致程序错误。详细的说明请参考《Java并发编程:深入剖析ThreadLocal》《ThreadLocal-面试必问深度解析》

我们利用ThreadLocalTaskFactory类做一些改进:

public class TaskFactory {
    private static ThreadLocal<Integer> taskHolder = new ThreadLocal<>();  //注①
    private Runnable runnable;
    
    public void build(int task){  //注②
        runnable = () -> {
        	taskHolder.set(task);  //注③
            System.out.print(String.valueOf(taskHolder.get()));  //注④
            taskHolder.remove();  //注⑤
        };
    }
    
    public Runnable getRunnable() {
        return runnable;
    }
}

注释:
①定义一个静态的ThreadLocal用来存储每个线程的任务;
②删除setTask函数,从build函数的参数里传入任务;
③设置当前线程的任务;
④取用任务
⑤为了避免任务长时间执行时,GC无法完全回收内存造成内存泄漏,在用完之后及时删除当前的变量副本。


后记

1.为什么不给每个线程都创建一个工厂?

其实在知道ThreadLocal之前,我就是这样处理的,让每一个TaskFactory的实例对应一个Runnable虽然也能解决问题,但是这样总觉得别扭,因为我们的工厂失去了它的意义,工厂变成了仓库,或者说“沙盒”。

2. 只要在build函数的参数里传入任务,不需要ThreadLocal也能实现需要?

如果我们让任务仍然是int taskbuild函数变成下面这个样子:

    public void build(int task){
        runnable = () -> { 
            System.out.print(String.valueOf(task));
        };
    }

按理说这样每个Runnable就有属于自己的task了,但是经过检验,我发现所有的Runnable在执行的时候,输出都为空,即任务信息丢失了。

经过分析我认为,task被传给Runnable之后,因为某种原因(可能是因为定时任务的缘故,变量有较长的时间未被引用)被GC判定为不可达,然后被回收了。因此task还是需要一个“容器”的,用工厂做也行,但是更好的选择是用ThreadLocal来维护它,不会丢,也不会发生混乱。

3. 对ThreadLocal的三点总结

①每个ThreadLocal只能保存一个变量副本,如果想要一个线程能够保存多个副本,就需要创建多个ThreadLocal
ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。因此在用完之后应该及时调用remove函数,使键值对同时变成不可达,进而可以被GC
ThreadLocal适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

IMplementist

你的鼓励,是我继续写文章的动力

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

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

打赏作者

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

抵扣说明:

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

余额充值