章节索引
前提
这篇博文解决的是上次重构我的日报系统时遗留下的问题,关于日报系统的简要说明请参阅《记一次重构》.
任务工厂
根据需求,我需要创建一个任务工厂类,用来给定时线程构建任务:
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-面试必问深度解析》。
我们利用ThreadLocal
对TaskFactory
类做一些改进:
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 task
,build
函数变成下面这个样子:
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
解决,需要另寻解决方案。