## 44 场景实战:ThreadLocal 在上下文传值场景下的实践
## 开篇语
我们在 《打动面试官:线程池流程编排中的运用实战》一文中将流程引擎简单地完善了一下,本文在其基础上继续进行改造,建议同学可以先看看 GitHub 上的代码,或者看看之前的文章。
### 1 回顾
流程引擎编排的对象,我们称为组件(就是 SpringBean),之前我们给组件定义了通用的接口,组件实现时就实现这个接口,代码如下:
![](https://img.kancloud.cn/f0/1c/f01c10b48f55c441e595e23cabd62758_1639x1246.jpg)
我们定义了 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 = CACHE.get(); 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 = CACHE.get(); if(null == cacheMap){ return null;
} return cacheMap.get(sourceKey); } }
```
如果你想往 ThreadLocal 放数据,调用 ContextCache.putAttribute 方法,如果想从 ThreadLocal 拿数据,调用 ContextCache.getAttribute 方法即可。
我们写了两个组件,一个组件放数据,一个组件拿数据,如下:
![](https://img.kancloud.cn/67/b1/67b17e3d22be24346c9eabf2d536966f_1447x1256.jpg)
我们把两个 SpringBean 注册到流程注册中心中,让其按照先执行 BeanThree 再执行 BeanFive 的顺序进行执行,运行 DemoApplication 类的 main 方法进行执行,执行结果如下:
![](https://img.kancloud.cn/d6/57/d6579a937814a44f72584cc2b54ed1da_1778x228.jpg)
从打印的日志可以看到,在 Spring 容器管理的 SpringBean 中,ThreadLocal 也是可以储存中间缓存值的。
### 3 开启子线程
我们做一个实验,我们在 BeanFive 中开启子线程,然后再从 ThreadLocal 中拿值,看看能否拿到值,BeanFive 的代码修改成如下:
![](https://img.kancloud.cn/44/b5/44b5cef6dbc8c35102d6366ba569136b_1704x665.jpg)
我们再来运行一下,打印的日志如下:
![](https://img.kancloud.cn/8b/c0/8bc0a61d0cc847b0112ad8ebcf8db3a5_2001x500.jpg)
从打印的日志中,我们发现在子线程中从 ThreadLocal 取值时,并没有取得值,这个原因主要是我们之前说的,线程在创建的时候,并不会把父线程的 ThreadLocal 中的值拷贝给子线程的
ThreadLocal,解决方案就是把 ThreadLocal 修改成 InheritableThreadLocal,代码修改如下:
![](https://img.kancloud.cn/7e/5a/7e5a3690442dd87fb2ac9b2ab09ea910_2036x392.jpg)
我们再次运行,结果如下:
![](https://img.kancloud.cn/44/07/4407e9f841c0dcdb36a8b00c329080a1_1498x216.jpg)
从运行结果看,我们成功的在子线程中拿到值。
### 4 线程池 + ThreadLocal
如果是拿数据的 springBean 是丢给线程池执行的,我们能够成功的从 ThreadLocal 中拿到数据么?
首先我们在放数据的 springBean 中,把放的值修改成随机的,接着拿数据的 SpringBean 修改成异步执行,代码修改如下:
![](https://img.kancloud.cn/bc/87/bc878391f933325ffa598c1dfe2be1aa_1920x1303.jpg)
为了能快速看到效果,我们把线程池的 coreSize 和 maxSize 全部修改成 3,并让任务沉睡一段时间,这样三个线程肯定消费不完任务,大量任务都会到队列中去排队,我们修改一下测试脚本,如下:
![](https://img.kancloud.cn/e7/82/e782c1d8552ac15d042724243bae8bc1_1394x340.jpg)
我们期望的结果:
1. 线程池中执行的 BeanFive 可以成功从 ThreadLocal 中拿到数据;
2. 能够从 ThreadLocal 拿到正确的数据,比如 BeanThree 刚放进 key1,value5,那么期望在 BeanFive 中根据 key1 能拿出 value5,而不是其它值。
我们运行一下,结果如下:
![](https://img.kancloud.cn/c0/b3/c0b3cf5d50a333ed870d577442edd12d_2024x874.jpg)
从结果中可以看到,并没有符合我们的预期,我们往 ThreadLocal 中 put 进很多值,但最后拿出来的值却很多都是 value379,都为最后 put 到 ThreadLocal 中的值。
这个原因主要是 ThreadLocal 存储的 HashMap 的引用都是同一个,main 主线程可以修改 HashMap 中的值,子线程从 ThreadLocal 中拿值时,也是从 HashMap 中拿值,从而导致不能把 put 的值通过 ThreadLocal 正确的传递给子线程。
为了证明是这个原因,我们在从 ThreadLocal 放、拿值的地方,把 HashMap 的内存地址都打印出来,改动代码如下:
![](https://img.kancloud.cn/b0/d1/b0d1a3dc8f1616645fce95b84b80288d_1809x1349.jpg)
我们再次运行测试代码,运行的结果如下:
![](https://img.kancloud.cn/86/22/8622db2dd1dbc42855a7f370a4b6f5e0_1015x820.jpg)
从测试结果中可以看到,不管是主线程还是子线程和 ThreadLocal 进行交互时,HashMap 都是同一个,也就是说 ThreadLocal 中保存的 HashMap 是共享的,这就导致了线程安全的问题,子线程读取到的值就会混乱掉。
### 5 解决方案
针对这个问题,我们提出了一种解决方案,在把任务提交到线程池时,我们进行 HashMap 的拷贝,这样子线程的 HashMap 和 main 线程的 HashMap 就不同了,可以解决上面的问题。
我们提交任务时, 使用的是 Runnable,要实现 HashMap 的拷贝的话,我们需要把 Runnable 进行一层包装,包装的代码如下:
![](https://img.kancloud.cn/16/94/1694b6ad1e35aab18bcf911311320480_800x913.jpg)
运行结果如下:
![](https://img.kancloud.cn/0e/70/0e70402815ac7488bd7b42d9e2f00720_934x820.jpg)
从运行结果中可以看出,线程池拿出来的 value 已经是正确的了。
- 前言
- 第1章 基础
- 01 开篇词:为什么学习本专栏
- 02 String、Long 源码解析和面试题
- 03 Java 常用关键字理解
- 04 Arrays、Collections、Objects 常用方法源码解析
- 第2章 集合
- 05 ArrayList 源码解析和设计思路
- 06 LinkedList 源码解析
- 07 List 源码会问哪些面试题
- 08 HashMap 源码解析
- 09 TreeMap 和 LinkedHashMap 核心源码解析
- 10 Map源码会问哪些面试题
- 11 HashSet、TreeSet 源码解析
- 12 彰显细节:看集合源码对我们实际工作的帮助和应用
- 13 差异对比:集合在 Java 7 和 8 有何不同和改进
- 14 简化工作:Guava Lists Maps 实际工作运用和源码
- 第3章 并发集合类
- 15 CopyOnWriteArrayList 源码解析和设计思路
- 16 ConcurrentHashMap 源码解析和设计思路
- 17 并发 List、Map源码面试题
- 18 场景集合:并发 List、Map的应用场景
- 第4章 队列
- 19 LinkedBlockingQueue 源码解析
- 20 SynchronousQueue 源码解析
- 21 DelayQueue 源码解析
- 22 ArrayBlockingQueue 源码解析
- 23 队列在源码方面的面试题
- 24 举一反三:队列在 Java 其它源码中的应用
- 25 整体设计:队列设计思想、工作中使用场景
- 26 惊叹面试官:由浅入深手写队列
- 第5章 线程
- 27 Thread 源码解析
- 28 Future、ExecutorService 源码解析
- 29 押宝线程源码面试题
- 第6章 锁
- 30 AbstractQueuedSynchronizer 源码解析(上)
- 31 AbstractQueuedSynchronizer 源码解析(下)
- 32 ReentrantLock 源码解析
- 33 CountDownLatch、Atomic 等其它源码解析
- 34 只求问倒:连环相扣系列锁面试题
- 35 经验总结:各种锁在工作中使用场景和细节
- 36 从容不迫:重写锁的设计结构和细节
- 第7章 线程池
- 37 ThreadPoolExecutor 源码解析
- 38 线程池源码面试题
- 39 经验总结:不同场景,如何使用线程池
- 40 打动面试官:线程池流程编排中的运用实战
- 第8章 Lambda 流
- 41 突破难点:如何看 Lambda 源码
- 42 常用的 Lambda 表达式使用场景解析和应用
- 第9章 其他
- 43 ThreadLocal 源码解析
- 44 场景实战:ThreadLocal 在上下文传值场景下的实践
- 45 Socket 源码及面试题
- 46 ServerSocket 源码及面试题
- 47 工作实战:Socket 结合线程池的使用
- 第10章 专栏总结
- 48 一起看过的 Java 源码和面试真题