⾯试官系统精讲Java源码及⼤⼚真题-44场景实战:
ThreadLocal在上下⽂传值场景下的实践
44 场景实战:ThreadLocal 在上下⽂传值场景下的实践
开篇语
我们在 《打动⾯试官:线程池流程编排中的运⽤实战》⼀⽂中将流程引擎简单地完善了⼀下,本⽂在其基础上继续进⾏改造,建议同学可以先看看 GitHub 上的代码,或者看看之前的⽂章。
1 回顾
流程引擎编排的对象,我们称为组件(就是 SpringBean),之前我们给组件定义了通⽤的接⼝,组件实现时就实现这个接⼝,代码如下:
我们定义了 DomainAbilityBean 接⼝,⼊参和出参都是 FlowContent,FlowContent 我们称为上下⽂。
2 ThreadLocal 实现
上下⽂传参除了 FlowContent 实现外,ThreadLocal 也是可以实现的,我们来演⽰⼀下:
2.1 定义 ThreadLocal 上下⽂⼯具类
⾸先我们使⽤ ThreadLocal 定义了上下⽂⼯具类,并且定义了 put、get ⽅法,⽅便使⽤,代码如下:
public class ContextCache implements Serializable {
private static final long serialVersionUID = 2136539028591849277L;
// 使⽤ ThreadLocal 缓存上下⽂信息
public static final ThreadLocal<Map<String,String>> CACHE = new ThreadLocal<>();
/**
* 放数据
* @param sourceKey
*/
public static final void putAttribute(String sourceKey,String value){
Map<String,String> cacheMap = ();
if(null == cacheMap){
cacheMap = new HashMap<>();
}
cacheMap.put(sourceKey,value);
CACHE.set(cacheMap);
}
/**
* 拿数据
* @param sourceKey
*/
public static final String getAttribute(String sourceKey){
Map<String,String> cacheMap = ();
if(null == cacheMap){
return null;
}
(sourceKey);
}
}
如果你想往 ThreadLocal 放数据,调⽤ ContextCache.putAttribute ⽅法,如果想从 ThreadLocal 拿数据,调⽤Attribute ⽅法即可。
我们写了两个组件,⼀个组件放数据,⼀个组件拿数据,如下:
我们把两个 SpringBean 注册到流程注册中⼼中,让其按照先执⾏ BeanThree 再执⾏ BeanFive 的顺序进⾏执⾏,运⾏DemoApplication 类的 main ⽅法进⾏执⾏,执⾏结果如下:
从打印的⽇志可以看到,在 Spring 容器管理的 SpringBean 中,ThreadLocal 也是可以储存中间缓存值的。
3 开启⼦线程
我们做⼀个实验,我们在 BeanFive 中开启⼦线程,然后再从 ThreadLocal 中拿值,看看能否拿到值,BeanFive 的代码修改成如下:
我们再来运⾏⼀下,打印的⽇志如下:
从打印的⽇志中,我们发现在⼦线程中从 ThreadLocal 取值时,并没有取得值,这个原因主要是我们之前说的,线程在创建的时候,并不会把⽗线程的 ThreadLocal 中的值拷贝给⼦线程的 ThreadLocal,解决⽅案就是把 ThreadLocal 修改成 InheritableThreadLocal,代码修改如下:
我们再次运⾏,结果如下:
从运⾏结果看,我们成功的在⼦线程中拿到值。
4 线程池 + ThreadLocal
如果是拿数据的 springBean 是丢给线程池执⾏的,我们能够成功的从 ThreadLocal 中拿到数据么?
⾸先我们在放数据的 springBean 中,把放的值修改成随机的,接着拿数据的 SpringBean 修改成异步执⾏,代码修改如下:
为了能快速看到效果,我们把线程池的 coreSize 和 maxSize 全部修改成 3,并让任务沉睡⼀段时间,这样三个线程肯定消费不完任务,⼤量任务都会到队列中去排队,我们修改⼀下测试脚本,如下:
我们期望的结果:
1. 线程池中执⾏的 BeanFive 可以成功从 ThreadLocal 中拿到数据;
2. 能够从 ThreadLocal 拿到正确的数据,⽐如 BeanThree 刚放进 key1,value5,那么期望在 BeanFive 中根据 key1 能拿出
value5,⽽不是其它值。
我们运⾏⼀下,结果如下:
从结果中可以看到,并没有符合我们的预期,我们往 ThreadLocal 中 put 进很多值,但最后拿出来的值却很多都是 value379,都为最后put 到 ThreadLocal 中的值。
这个原因主要是 ThreadLocal 存储的 HashMap 的引⽤都是同⼀个,main 主线程可以修改 HashMap 中的值,⼦线程从 ThreadLocal 中拿值时,也是从 HashMap 中拿值,从⽽导致不能把 put 的值通过 ThreadLocal 正确的传递给⼦线程。
为了证明是这个原因,我们在从 ThreadLocal 放、拿值的地⽅,把 HashMap 的内存地址都打印出来,改动代码如下:
我们再次运⾏测试代码,运⾏的结果如下:
从测试结果中可以看到,不管是主线程还是⼦线程和 ThreadLocal 进⾏交互时,HashMap 都是同⼀个,也就是说 ThreadLocal 中保存的 HashMap 是共享的,这就导致了线程安全的问题,⼦线程读取到的值就会混乱掉。
java上下文context
5 解决⽅案
针对这个问题,我们提出了⼀种解决⽅案,在把任务提交到线程池时,我们进⾏ HashMap 的拷贝,这样⼦线程的 HashMap 和 main 线程的 HashMap 就不同了,可以解决上⾯的问题。
我们提交任务时, 使⽤的是 Runnable,要实现 HashMap 的拷贝的话,我们需要把 Runnable 进⾏⼀层包装,包装的代码如下:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。