父子线程之间值传递解决方案:InheritableThreadLocal和TransmittableThreadLocal

349 阅读5分钟

1.InheritableThreadLocal

之前我们讲述过ThreadLocal的实现原理和使用场景、方式,可以看出ThreadLocal是相对于每一个线程自己使用的本地变量,但是在实际的开发中,有这样的一种需求:父线程生成的变量需要传递到子线程中进行使用,那么在使用ThreadLocal似乎就解决不了这个问题,ThreadLocal有一个子类InheritableThreadLocal就是为了解决这个问题而产生的,使用这个变量就可以轻松的在子线程中依旧使用父线程中的本地变量。

所以可以用一句总结InheritableThreadLocal就是用来解决父子线程之间变量传递的问题。

2.使用示例

package com.shepherd.example.juc;
​
/**
 * @author fjzheng
 * @version 1.0
 * @date 2022/4/25 16:26
 *//**
 * 使用ThreadLocal的时候,在异步场景下是无法给子线程共享父线程中创建的线程副本数据的
 */
public class InheritableThreadLocalDemo {
    public static void main(String[] args) {
        ThreadLocal<String> ThreadLocal = new ThreadLocal<>();
        ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
        ThreadLocal.set("父线程数据:threadLocal");
        inheritableThreadLocal.set("父线程数据:inheritableThreadLocal");
​
        new Thread(()->{
            System.out.println("子线程获取父类ThreadLocal数据:" + ThreadLocal.get());
            System.out.println("子线程获取父类inheritableThreadLocal数据:" + inheritableThreadLocal.get());
        }).start();
    }
}
​

结果如下:

子线程获取父类ThreadLocal数据:null
子线程获取父类inheritableThreadLocal数据:父线程数据:inheritableThreadLocal

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址github.com/plasticene/…

Gitee地址gitee.com/plasticene3…

微信公众号Shepherd进阶笔记

交流探讨群:Shepherd_126

3.实现原理

实现原理是子线程是通过在父线程中通过调用new Thread()方法来创建子线程,Thread#init方法在Thread的构造方法中被调用。在init方法中拷贝父线程数据到子线程中

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
​
   private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

init方法

 private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
​
        this.name = name;
​
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */
​
            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }
​
            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
​
        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();
​
        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }
​
        g.addUnstarted();
​
        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        // 如果父线程的inheritableThreadLocals不为空,就赋值给当前新建的线程,实现变量传递
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;
​
        /* Set thread ID */
        tid = nextThreadID();
    }

InheritableThreadLocal仍然有缺陷,一般我们做异步化处理都是使用的线程池,而InheritableThreadLocal是在new Thread中的init()方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。多线程变量传递问题示例如下:

public class InheritableThreadLocalFailDemo {
​
    // 创建线程池
    private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
​
​
    public static void main(String[] args) {
        //TransmittableThreadLocal<String> context = new TransmittableThreadLocal<String>();
        InheritableThreadLocal<String> context = new InheritableThreadLocal<>();
        context.set("value1-set-in-parent");
        fixedThreadPool.submit(()->{
            // 第一个子线程打印value1-set-in-parent,毋庸置疑
            System.out.println(context.get());
        });
        // 父线程修改InheritableThreadLocal变量值
        context.set("value2-set-in-parent");
        // 再次新建一个子线程,这时候就要看这个子线程是新建的,还是复用线程池里面的之前的线程
        // 如上面线程池核心数和最大线程数都为1,说明线程池只能创建一个线程,此时这个子线程会复用前面那个子线程,这时候InheritableThreadLocal
        // 的变量值就等于前面那个线程的,所以这里和第一个子线程一样打印value1-set-in-parent
        // 如果上面线程池核心数和最大线程数都为2,这时候会新建一个子线程,此时会获取父线程的最新值,打印value2-set-in-parent
        fixedThreadPool.submit(()->{
            System.out.println(context.get());
        });
    }
}

当然,有问题出现就会有解决问题的方案,阿里巴巴开源了一个TransmittableThreadLocal组件就可以解决这个问题

4.TransmittableThreadLocal

JDKInheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时ThreadLocal值传递到 任务执行时

使用示例:

package com.shepherd.example.juc;
​
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.threadpool.TtlExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.Data;
​
import java.util.concurrent.*;
​
/**
 * @author fjzheng
 * @version 1.0
 * @date 2022/4/25 19:04
 */
public class TransmittableThreadLocalTest {
    private static ExecutorService executorService =
            TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
​
    // TTL
    private static TransmittableThreadLocal<LocalBean> ttl = new TransmittableThreadLocal();
​
    public static void main(String[] args) throws InterruptedException {
        LocalBean localBean = new LocalBean("初始值1");
        ttl.set(localBean); // 在主线程设置初始值
        new Thread(() -> {
            System.out.println("对普通线程的传递性:" + ttl.get());
            localBean.setProp("单一线程更新值2");
        }).start();
        // 1、线程池中取值,设置对象内的属性值,测试是否影响外部线程
        executorService.execute(() -> {
            LocalBean localBean1 = ttl.get();
            System.out.println(String.format("线程名称(%s): %s", Thread.currentThread().getName(), localBean1.getProp()));
            localBean1.setProp("线程池更新值3");
            System.out.println(String.format("After set, 线程名称(%s): %s", Thread.currentThread().getName(), ttl.get().getProp()));
        });
​
        TimeUnit.SECONDS.sleep(1);
        System.out.println(String.format("Main 线程名称(%s): %s", Thread.currentThread().getName(), ttl.get().getProp()));
        // 2、修改对象引用,测试是否影响外部线程
        executorService.execute(() -> {
            System.out.println(String.format("线程名称(%s): %s", Thread.currentThread().getName(), ttl.get().getProp()));
            ttl.set(new LocalBean("线程池替换localBean"));
        });
​
        TimeUnit.SECONDS.sleep(1);
        System.out.println(String.format("main 线程名称(%s): %s", Thread.currentThread().getName(), ttl.get()));
    }
​
​
    @Data
    static class LocalBean {
        private String prop;
​
        LocalBean(String p) {
            this.prop = p;
        }
    }
}
​

执行结果如下:

对普通线程的传递性:TransmittableThreadLocalTest.LocalBean(prop=初始值1)
线程名称(pool-1-thread-1): 单一线程更新值2
After set, 线程名称(pool-1-thread-1): 线程池更新值3
Main 线程名称(main): 线程池更新值3
线程名称(pool-1-thread-1): 线程池更新值3
main 线程名称(main): TransmittableThreadLocalTest.LocalBean(prop=线程池更新值3)

根据结果显示,TransmittableThreadLocal正常在线程池的线程之间传递了变量值。

TransmittableThreadLocal(TTL)实现线程变量传递的原理,可以参考 TTL实现原理,也可以直接浏览官网