媒介:在Java中利用ThreadLocal类时,怎么实现父子线程直接的值传递呢?
假设这样利用会有问题吗?- public class Main {
- private static final ThreadLocal<String> threadlocal = new ThreadLocal<>();
- public static void main(String[] args) {
- threadlocal.set("父线程设置");
- new Thread( () -> {
- System.out.println("子线程读取" + threadlocal.get());
- }).start();
- System.out.println();
- }
- }
复制代码 假如直接这样写的话是获取不到父线程的值的。
怎么解决呢
这里介绍俩种方式;
InheritableThreadLocal类
利用jdk自带的,InheritableThreadLocal- public class Main {
- // 使用InheritableThreadLocal
- private static final InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
- public static void main(String[] args) {
- threadLocal.set("父线程设置");
- new Thread( () -> {
- System.out.println("子线程读取\t" + threadLocal.get());
- }).start();
- System.out.println();
- }
- }
复制代码
利用这个类实现threadlocal后可以发现可以读取到了;
那么是怎么实现的呢?
紧张就是这俩个机制:线程创建时的值拷贝机制 和 Thread类的特殊计划
进入源码查看,- <strong>首先:InheritableThreadLocal集成了ThreadLocal类。</strong>
复制代码- public class InheritableThreadLocal<T> extends ThreadLocal<T> {
-
- public InheritableThreadLocal() {}
- // 重写childValue方法(可自定义继承逻辑)
- protected T childValue(T parentValue) {
- return parentValue;
- }
- // // 关键方法:获取线程的inheritableThreadLocals变量
- ThreadLocalMap getMap(Thread t) {
- return t.inheritableThreadLocals;
- }
-
- void createMap(Thread t, T firstValue) {
- t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
- }
- }
复制代码
这是Thread类的一些片断,截取出来了,紧张看解释的地方、在init()方法中- class Thread {
- // 普通ThreadLocal存储
- ThreadLocal.ThreadLocalMap threadLocals;
-
- // 专门用于继承的ThreadLocal存储
- ThreadLocal.ThreadLocalMap inheritableThreadLocals;
-
- // 线程初始化方法
- private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
- init(g, target, name, stackSize, null, true);
- }
-
- //在init方法中,使用if判断了一下父线程是不是null的,就是检查父线程的
- //inheritableThreadLocals,如果存在父线程则会触发拷贝的逻辑。
- //会调用createInheritedMap这个方法,子线程就可以获得一个新的ThreadLoaclMap对象。
- //这样就完成了拷贝的过程
- //if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
- // 关键点:创建时拷贝父线程的inheritableThreadLocals
- //this.inheritableThreadLocals =
- // ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
- //}
- private void init(..., boolean inheritThreadLocals) {
- Thread parent = currentThread();
- if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
- // 关键点:创建时拷贝父线程的inheritableThreadLocals
- this.inheritableThreadLocals =
- ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
- }
- // ...其他初始化
- }
- }
复制代码 createInheritedMap源码解读- //调用createInheritedMap时,会将父线程的ThreadLocalMap对象传入过去
- //此时就会遍历map对象逐个复制entry到当前线程的ThreadLocalMap中去。
- static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
- // 创建新Map并逐个复制Entry
- //创建了一个全新的ThreadLocalMap对象,而不是简单的引用传递
- ThreadLocalMap map = new ThreadLocalMap();
- for (Entry e : parentMap.getTable()) {
- if (e != null) {
- @SuppressWarnings("unchecked")
- ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
- if (key != null) {
- // 调用childValue方法处理继承值!
- //这里我们可以重新childValue方法实现修改父线程的值。
- /*
- InheritableThreadLocal<String> itl = new InheritableThreadLocal<String>() {
- @Override
- protected String childValue(String parentValue) {
- return "Child_" + parentValue; // 可修改继承的值
- }
- };
- */
- Object value = key.childValue(e.value);
- map.set(key, value);
- }
- }
- }
- return map;
- }
- //此后子线程读父线程的值的时候,就可以读取到拷贝的值了。
复制代码 以上就是全部的过程了。这样就通过继续的方式实现了一个可传递父子线程值的类,
同时,我们根据上面的过程分析,可以发现照旧有很多不敷的;
其一由于是通过for循环遍历方式来拷贝的,假如父线程的InheritableThreadLocal中有大量数据,会低落线程创建速率。
其二有内存走漏风险,和ThreadLocal一样必要手动remove(),否则可能导致:子线程长期持有父线程的引用,大对象无法被GC。
其三:最紧张的是在线程池环境下,由于线程的复用,而不是new Thread()这样的方式创建,从而不会触发init()方法,但是拷贝的过程在init()方法中,以是在线程复用的环境下会失效。
TransmittableThreadLocal类
TransmittableThreadLocal (TTL) 是阿里巴巴开源的一个线程当地变量解决方案,它解决了 InheritableThreadLocal 在线程池环境下无法正确传递值的问题。
初始化过程,- public static final TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal<>();
- //还需要使用TtlExecutors.getTtlExecutorService()对我们的线程池做了增强(这也是必须的搭配,否则
- //没法使用 TransmittableThreadLocal 特性)
- private static ExecutorService businessExecutors = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(5));
复制代码 可以点进去看详细的源码,可以望见详细的布局- //可以看见继承了java.lang.InheritableThreadLocal<T>
- //还实现了com.alibaba.ttl.TtlCopier<T>这个类
- public class TransmittableThreadLocal <T>
- extends java.lang.InheritableThreadLocal<T>
- implements com.alibaba.ttl.TtlCopier<T> {
- /***/
- }
- package com.alibaba.ttl;
- //可以看见TtlCopier是一个函数式接口
- @java.lang.FunctionalInterface
- public interface TtlCopier <T> {
- T copy(T t);
- }
复制代码 详细是怎么实现的呢?
分为4步过程:
第一步:父线程设置值(ThreadLocal.set)
- TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
- context.set("parent-value"); // 设置父线程的值
复制代码 他的存储布局是什么样的呢?- //维护了一个 全局 holder(WeakHashMap)来跟踪所有 TTL实例:
- static ThreadLocal<Map<TransmittableThreadLocal<Object>, ?>> holder =
- new ThreadLocal<Map<TransmittableThreadLocal<Object>, ?>>() {
- @Override
- protected Map<TransmittableThreadLocal<Object>, ?> initialValue() {
- return new WeakHashMap<>(); // 弱引用防止内存泄漏
- }
- };
- //调用set时,重写了InheritableThreadLocal的set方法,
- //这里的disableIgnoreNullValueSemantics && value == null语句中
- //disableIgnoreNullValueSemantics指的是 是否禁用空值语义 。控制 null 值的存储行为
- //如果允许的话,value == null 就 不走remove方法,如果不允许并且value == null就会走remove方法
- @Override
- public final void set(T value) {
- if (!disableIgnoreNullValueSemantics && value == null) {
- // may set null to remove value
- remove();
- } else {
- super.set(value);
- addThisToHolder();
- }
- }
复制代码 第二步:提交使命时获取值
- //当向线程池提交任务时,TTL 会 捕获当前线程的所有 TTL 值:
- ExecutorService executor = Executors.newFixedThreadPool(1);
- executor.execute(TtlRunnable.get(() -> {
- // 子线程逻辑
- }));
- //关键方法 TtlRunnable.get():
- public static Runnable get(Runnable runnable) {
- // 1. 捕获当前线程的所有 TTL 值(快照)
- Map<TransmittableThreadLocal<Object>, Object> captured = capture();
- return new TtlRunnable(runnable, captured);
- }
- //capture() 源码:此时,captured 是一个 键值对快照,保存了所有 TTL 的当前值。
- static Map<TransmittableThreadLocal<Object>, Object> capture() {
- Map<TransmittableThreadLocal<Object>, Object> captured = new HashMap<>();
- // 遍历 holder 中所有 TTL 实例,拷贝它们的值
- for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
- captured.put(threadLocal, threadLocal.copyValue()); // 深拷贝值
- }
- return captured;
- }
复制代码 第三步:子线程实行前规复值(replay)
- //当线程池中的线程执行任务时,TTL 会 将捕获的值恢复到当前线程:
- public void run() {
- Map<TransmittableThreadLocal<Object>, Object> backup = replay(captured);
- try {
- runnable.run(); // 执行用户任务
- } finally {
- restore(backup); // 恢复线程原始状态
- }
- }
- //为什么需要replay呢
- //因为在子线程中可能会修改ThreadLocal的值,另外restore里面会主动调用remove()回收,
- //避免内存泄露(会删除子线程新增的TTL)
- //有下列两种情况:
- //1. 一种情况是:主线程启动了一个异步任务,此时主线程和子线程会并行,
- //由于父子线程的数据是隔离开的,子线程此时对TTL中的内容进行修改并不会影响到原线程的逻辑
- //2. 另一种情况是:线程池的拒绝策略为CallerRunsPolicy时,
- //那么在主线程内启动这个异步任务可能会有当前的主线程来执行,
- //那么线程之间的数据并不会隔离,那么如果对ThreadLocal中的数据进行了修改,
- //那么将会影响到程序的正常运行。
- //replay() 源码:
- //先备份当前线程的原始值(防止污染)。
- //清除当前线程的 TTL 值(避免旧值干扰)。
- //将父线程的快照值设置到当前线程。
- static Map<TransmittableThreadLocal<Object>, Object> replay(
- Map<TransmittableThreadLocal<Object>, Object> captured) {
-
- // 1. 备份当前线程的原始 TTL 值
- Map<TransmittableThreadLocal<Object>, Object> backup = new HashMap<>();
- for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
- backup.put(threadLocal, threadLocal.get());
- }
- // 2. 清除当前线程的所有 TTL 值
- for (TransmittableThreadLocal<Object> threadLocal : captured.keySet()) {
- threadLocal.superRemove(); // 调用 InheritableThreadLocal.remove
- }
- // 3. 将捕获的父线程值设置到当前线程
- for (Map.Entry<TransmittableThreadLocal<Object>, Object> entry : captured.entrySet()) {
- entry.getKey().set(entry.getValue()); // 调用 TTL.set
- }
- return backup; // 返回备份的原始值
- }
复制代码 第四步:子线程读取值(TTL.get)
- //在任务执行期间,子线程通过 TTL.get() 读取到的值 来自父线程的快照:
- executor.execute(TtlRunnable.get(() -> {
- System.out.println(context.get()); // 输出 "parent-value"
- }));
- //get方法源码,重写了父类的get方法,
- //由于之前通过 replay() 设置了值,这里会正确返回父线程传递的值
- @Override
- public final T get() {
- T value = super.get();
- //判断是否允许null的存在,默认是false的。前面介绍果。
- if (disableIgnoreNullValueSemantics || value != null) addThisToHolder();
- return value;
- }
复制代码 第五步:使命实行后清算(restore)
- //任务执行完成后,TTL 会 恢复线程的原始状态,避免影响后续任务:
- //确保线程池中的线程在执行完任务后,不会残留本次任务的 TTL 值,避免内存泄漏和值污染。
- static void restore(Map<TransmittableThreadLocal<Object>, Object> backup) {
- // 1. 清除当前线程的所有 TTL 值
- for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
- threadLocal.superRemove();
- }
- // 2. 还原备份的原始值
- for (Map.Entry<TransmittableThreadLocal<Object>, Object> entry : backup.entrySet()) {
- entry.getKey().set(entry.getValue());
- }
- }
复制代码 以上就是实现的全部过程了,此中他还包装一些类,
包装机制和值快照技能,美满解决了线程池环境下的上下文传递问题,是Java异步编程中不可或缺的工具。- // 包装普通Runnable
- public static Runnable wrap(Runnable runnable) {
- Map<TransmittableThreadLocal<Object>, Object> captured = capture();
- return () -> {
- Map<TransmittableThreadLocal<Object>, Object> backup = replay(captured);
- try {
- runnable.run();
- } finally {
- restore(backup);
- }
- };
- }
复制代码 在capture/replay这俩个方法中有肯定开销,由于是for循环遍向来操纵数据的。我们必要克制存储大对象。
并且TTL是存在线程安全问题的,由于默认都是引用范例拷贝,假如子线程修改了数据,主线程是可以感知到的。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
|