美文网首页
多线程篇-父子线程的上下文传递

多线程篇-父子线程的上下文传递

作者: sharedCode | 来源:发表于2020-11-21 15:36 被阅读0次

    前言

    在实际的工作中,有些时候我们会遇到一些线程之间共享数据的问题,比如一下几个典型的业务场景,举几个例子,大家能够更加直观的感受到

    1. 分布式链路追踪系统

      这个不必多说,如果我们需要记录系统的每个调用链路,那么每一个子系统里面,如果调用了异步线程来做处理的话,那么类似这种链路是不是需要收集起来呢?

    2. 日志收集记录系统上下文

      在实际的日志打印记录中,一个http请求进来的话,每一行日志,日志产生的线程信息?上下文信息,是不是需要记录下来呢?

    上面我举的是我们最最最常见的两个例子了, 做了几年开发的都能理解为啥要有这个东西,下面我们仔细聊一下这个问题,

    InheritableThreadLocal

    其实说到这个问题,有些同学就会想到InheritableThreadLocal 这个工具了,这是JDK给我们提供的的工具,该工具可以解决父子线程之间的值的传递,我们先来一个简单的demo, 然后再进行原理分析

    demo

    /**
     * ce
     *
     * @author zhangyunhe
     * @date 2020-04-22 16:19
     */
    public class InteritableTest2 {
    
    
        static ThreadLocal<String> local = new InheritableThreadLocal<>();
        // 初始化一个长度为1 的线程池
        static ExecutorService poolExecutor = Executors.newFixedThreadPool(1);
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
    
            InteritableTest2 test = new InteritableTest2();
            test.test();
        }
        private void test(){
            // 设置一个初始值
            local.set("天王老子");
            poolExecutor.submit(new Task());
    
        }
    
        class Task implements Runnable{
    
            @Override
            public void run() {
                // 子线程里面打印获取到的值
                System.out.println(Thread.currentThread().getName()+":"+local.get());
            }
        }
    }
    

    输出结果

    pool-1-thread-1:天王老子
    

    从上面可以看到, 子线程pool-1-thread-1可以获取到父线程在local里面设置的值,这就实现了值的传递了。

    源码分析

    下面我们从源码的角度上看一下InheritableThreadLocal的实现,他究竟是怎么做到父子线程之间线程的传递的。

    我们首先看一下Thread创建的代码。

    Thread

    线程初始化的代码,可以看到重点在init方法

    public Thread() {
            init(null, null, "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;
                // 1. 获取当前线程为父线程,其实就是创建这个线程的线程
            Thread parent = currentThread();
                    // 省略代码。。。。。
            // 2. 判断inheritThreadLocals 是否==true, 父节点的inheritableThreadLocals是否不为空
            if (inheritThreadLocals && parent.inheritableThreadLocals != null)
              //3. 符合以上的话,那么创建当前线程的inheritableThreadLocals
                this.inheritableThreadLocals =
                    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
            /* Stash the specified stack size in case the VM cares */
            this.stackSize = stackSize;
    
            /* Set thread ID */
            tid = nextThreadID();
        }
    

    步骤说明:

    看着我上面标的步骤进行说明

    1.获取当前线程为父线程,其实就是创建这个线程的线程

    2.判断inheritThreadLocals 是否==true , 默认inheritThreadLocals就是为true, 通用的new Thread()方法,这个值就是true, 同时判断父节点的inheritableThreadLocals是否为空, 如果不为空,则说明需要进行传递。

    3.在这个if里面,针对当前线程做了inheritableThreadLocals的初始化, 把父线程的值拷贝到这个里面来。

    通过上面的分析,其实基本的原理都已经了解清楚了,不熟悉的可以可以自己去细细研究。

    那么是否这种做法完全可以符合我们的需求呢? 我们看一下下面的场景

    线程池异常场景

    线程池demo

    /**
     * ce
     *
     * @author zhangyunhe
     * @date 2020-04-22 16:19
     */
    public class InteritableTest {
    
    
        static ThreadLocal<String> local = new InheritableThreadLocal<>();
    
        static ExecutorService poolExecutor = Executors.newFixedThreadPool(1);
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
    
            InteritableTest test = new InteritableTest();
            test.test();
        }
        private void test() throws ExecutionException, InterruptedException {
    
    
            local.set("天王老子");
            Future future = poolExecutor.submit(new Task("任务1"));
    
            future.get();
    
            Future future2 = poolExecutor.submit(new Task("任务2"));
    
            future2.get();
    
            System.out.println("父线程的值:"+local.get());
        }
    
        class Task implements Runnable{
    
            String str;
            Task(String str){
                this.str = str;
            }
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+":"+local.get());
                local.set(str);
                System.out.println(local.get());
            }
        }
    
    }
    

    输出结果:

    pool-1-thread-1:天王老子
    任务1
    pool-1-thread-1:任务1
    任务2
    父线程的值:天王老子
    

    从上面可以看到,Task2执行的时候,获取到的父线程的值是Task1修改过的。 这样感觉是不是就破坏了我们的本意? 实际上,这是因为我们使用了线程池,在池化技术里面,线程是会被复用的,当执行Task2的时候,实际上是用的Task1的那个线程,那个线程已经被创建好了的,所以那里面的locals就是被Task1修改过的,那么遇到这种问题,该如何解决呢?

    下一篇文章给大家介绍一个组件,在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能

    相关文章

      网友评论

          本文标题:多线程篇-父子线程的上下文传递

          本文链接:https://www.haomeiwen.com/subject/fzcdiktx.html