zoukankan      html  css  js  c++  java
  • 父线程与子线程传值问题

    一、ThreadLocal回顾

    ThreadLocal对象用于在同一个线程中传递数据,避免显式的在方法中传参。

    每个线程中保存了ThreadLocalMap对象,ThreadLocalMap对象的key就是ThreadLocal对象本身,value就是当前线程的值。

    看下ThreadLocal的get方法

    public T get() {
            //当前线程
            Thread t = Thread.currentThread();
            //获取当前线程的ThreadLocalMap对象
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                //获取该ThreadLocal对象的value
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            //设置初始值
            return setInitialValue();
        }
        
        //获取当前线程的ThreadLocalMap对象
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    View Code

    该方法首先从当前线程中获取ThreadLocalMap对象,接着从ThreadLocalMap获取该ThreadLocal锁对应的值;如果未获取到,调用setInitialValue方法,设置初始值,并返回初始值。再看下ThreadLocal的set方法

    public void set(T value) {
            //获取当前线程
            Thread t = Thread.currentThread();
            //获取当前线程的ThreadLocalMap对象
            ThreadLocalMap map = getMap(t);
            //如果ThreadLocalMap对象存在,则直接设置key(ThreadLocal对象),value;否则创建ThreadLocalMap对象,并设置key,value
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
        
         void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    View Code

    该方法同样获取当前线程的ThreadLocalMap对象,如果该对象不为空,那么设置key(ThreadLocal对象),value;否则创建ThreadLocalMap对象,并设置key,value

    二、父线程与子线程传值问题

    ThreadLocal无法将父线程中的值传递到子线程

    下面的代码在主线程中设置threadLocal的值为"dhytest",在子线程中调用get方法,聪明的你一定知道返回的是null. 因为在子线程中调用get方法,获取的是子线程中的ThreadLocalMap对象,而子线程中的ThreadLocalMap对象并未对key (threadLocal)设置相应的value

    static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
    public static void main(String[] args) {
    
        threadLocal.set("dhytest");
        
        new Thread(()->{
            System.out.println("子线程获取到的值:" + threadLocal.get());
        }).start();
    
        System.out.println("父线程获取到的值:" + threadLocal.get());
    }
    View Code

    运行结果:

    父线程获取到的值:dhytest
    子线程获取到的值:null
    View Code

    如何将父线程的值传递给子线程?

    方法一:

    在执行start方法前获取到父线程的值,因为在thread对象执行start方法前,当前线程还是父线程,因此可以通过threadLocal.get方法获取父线程的值

    static ThreadLocal<String> threadLocal = new ThreadLocal<>();
       
       private void test() throws InterruptedException {
    
            Thread.currentThread().setName("main-thread");
    
            //主线程设置一个值
            threadLocal.set(new Value("dhyTest"));
    
            //运行子线程
            Thread childThread = new Thread(new ParentChildTransferValue2.ChildThread(), "child-thread");
            childThread.start();
    
            //主线成等待子线程运行完,以便观察主线中设置的值是否被子线程成功修改
            childThread.join();
    
            System.out.println("父线程获取到的最终的值:" + threadLocal.get());
    
        }
    
        class ChildThread implements Runnable {
            //获取主线程中设置的值
            Value value = threadLocal.get();
    
            @Override
            public void run() {
    
                //打印主线程的值
                System.out.println("原父线程的值: " + value);
    
                //如果启用了线程(调用start方法),调用get方法是获取不到值的
                Value nullValue = threadLocal.get();
                System.out.println("子线程中直接调用get方法获取父线程的值,value:" + nullValue);
    
                //获取到父线程的值,并进行更改
                value.setData(value.getData() + "---子线程对父线程的值做了修改" );
            }
        }
    View Code

    运行结果:

    原父线程的值: dhyTest
    子线程中直接调用get方法获取父线程的值,value:null
    父线程获取到的最终的值:dhyTest---子线程对父线程的值做了修改

    方法二

    使用 InheritableThreadLocal

    static InheritableThreadLocal<Value> threadLocal = new InheritableThreadLocal<>();
    
        private void test() throws InterruptedException {
    
            Thread.currentThread().setName("main-thread");
    
            //主线程设置一个值
            threadLocal.set(new Value("dhyTest"));
    
            //运行子线程
            Thread childThread = new Thread(new ChildThread(), "child-thread");
            childThread.start();
    
            //主线成等待子线程运行完,以便观察主线中设置的值是否被子线程成功修改
            childThread.join();
    
            System.out.println("父线程获取到的最终的值:" + threadLocal.get());
    
        }
    
        class ChildThread implements Runnable {
            @Override
            public void run() {
                Value value = threadLocal.get();
                System.out.println("子线程中直接调用get方法获取父线程的值,value:" + value);
                value.setData(value.getData() + "---子线程对父线程的值做了修改");
            }
        }
    View Code

    运行结果:

    子线程中直接调用get方法获取父线程的值,value:dhyTest
    父线程获取到的最终的值:dhyTest---子线程对父线程的值做了修改
    View Code

    InheritableThreadLocal分析

    为什么使用InheritableThreadLocal,子线程就可以获取到父线程的值

    看下InheritableThreadLocal类,InheritableThreadLocal继承了ThreadLocal类,重写了childValue,getMap,createMap方法

    对于getMap方法,InheritableThreadLocal中返回的是线程中的inheritableThreadLocals变量,而ThreadLocal返回的是线程中的threadLocals变量;setMap同理

    public class InheritableThreadLocal<T> extends ThreadLocal<T> {
       
        protected T childValue(T parentValue) {
            return parentValue;
        }
        
        ThreadLocalMap getMap(Thread t) {
           return t.inheritableThreadLocals;
        }
    
        void createMap(Thread t, T firstValue) {
            t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
        }
    }
    View Code

    再看下Thread实例化的代码,从构造函数跟进init方法,inheritThreadLocals变量是true。在init方法中,获取父线程,将父线程的inheritableThreadLocals变量赋值给子线程的inheritableThreadLocals变量,从而实现了父线程与子线程的传值

    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);
        }
        
        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();
            
            //中间省略了一些代码
            
            //inheritThreadLocals 是true并且父线程的inheritableThreadLocals不为空,那么将父线程的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();
        }
    View Code

    具体看下是子线程是如何拷贝父线程的值的:

    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
            return new ThreadLocalMap(parentMap);
        }
        
        private ThreadLocalMap(ThreadLocalMap parentMap) {
                Entry[] parentTable = parentMap.table;
                int len = parentTable.length;
                setThreshold(len);
                table = new Entry[len];
    
                for (int j = 0; j < len; j++) {
                    Entry e = parentTable[j];
                    if (e != null) {
                        @SuppressWarnings("unchecked")
                        ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                        if (key != null) {
                            Object value = key.childValue(e.value);
                            Entry c = new Entry(key, value);
                            int h = key.threadLocalHashCode & (len - 1);
                            //如果发生hash碰撞,那么槽位后移
                            while (table[h] != null)
                                h = nextIndex(h, len);
                            table[h] = c;
                            size++;
                        }
                    }
                }
            }
    View Code

    InheritableThreadLocal存在的问题

    虽然InheritableThreadLocal可以解决在子线程中获取父线程的值的问题,但是在使用线程池的情况下,由于不同的任务有可能是同一个线程处理,因此这些任务取到的值有可能并不是父线程设置的值

    case1:模拟使用线程池情况下(为了便于测试不同任务有同一个线程处理的场景,使用单线程),两个任务由同一个线程处理。

    第一个任务中获取到父线程的值,并且重新设置了值;第二个任务中获取到的并不是父线程的值了,而是第一个任务设置的值。

    static InheritableThreadLocal<Value> threadLocal = new InheritableThreadLocal<>();
    static ExecutorService executorService = Executors.newSingleThreadExecutor();
    
    private void test() throws InterruptedException, ExecutionException {
    
            threadLocal.set(new Value("dhytest"));
    
            Future<?> task1 = executorService.submit((new ChildThread("task1")));
            task1.get();
    
            Future<?> task2 = executorService.submit(new ChildThread("task2"));
            task2.get();
    
                    System.out.println("父线程的值:" + threadLocal.get());
        }
    
        class ChildThread implements Runnable {
    
            private String taskName;
    
            ChildThread(String taskName) {
                this.taskName = taskName;
            }
    
            @Override
            public void run() {
                Value value = threadLocal.get();
                System.out.println("任务【" + taskName + "】获取到父线程的值为:" + value);
                threadLocal.set(new Value("值被任务【" + taskName + "】修改啦"));
                //如果使用下面的代码,那么父线程的值是会被改变的
                //value.set(new Value("父线程的值也被修改啦,因为是引用传递"));
            }
        }
    View Code

    运行结果:

    任务【task1】获取到父线程的值为:dhytest
    任务【task2】获取到父线程的值为:值被任务【task1】修改啦
    父线程的值:dhytest
    View Code

    为什么第二个任务获取到的是第一个任务设置的值,而没有获取到父线程原本的值?

    从实例化Thread的方法(init)中可以看出,实例化线程时,会检测是否需满足拷贝父线程的条件(inheritThreadLocals 是true并且父线程的inheritableThreadLocals不为空),若果满足,那么将父线程的inheritableThreadLocals变量拷贝给子线程的inheritableThrealLocals变量,也就是Thread类的ThreadLocal.createInheritedMap方法。

    执行第一个任务时,创建一个线程,执行初始化,将父线程的inheritableThreadLocals拷贝给了子任务;调用get(InheritableThreadLocal 继承了ThreadLocal,重写了getMap方法)方法,会返回给线程持有的inheritableThreadLocals变量;

    执行第二个任务时,由于使用的是同一个线程,因此调用get方法,返回的是这个线程持有的inheritableThreadLocals变量,而此时该变量中的value已被第一个任务改写,因此获取到并不是父线程原本的值

    虽然任务对value进行了重新赋值,但是并不影响父线程的值,因为value指向了一个新的地址。如果直接更改value,那么会影响父线程的值,因为指向的是同一个地址

    case2:使用线程池情况下,子任务由同一个线程处理,但是父线程是不同的线程

    private void test() throws InterruptedException, ExecutionException {
            //父线程1
            Thread parent1 = new Thread(() -> {
                threadLocal.set(new Value("parent1"));
                Future<?> task1 = executorService.submit((new ChildThread("task1-parent1")));
                try {
                    task1.get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            });
            parent1.start();
            parent1.join();
            //父线程2
            Thread parent2 = new Thread(() -> {
                threadLocal.set(new Value("parent2"));
                Future<?> task2 = executorService.submit((new ChildThread("task2-parent2")));
                try {
                    task2.get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            });
            parent2.start();
    
        }
    View Code

    运行结果:

    任务【task1-parent1】获取到父线程的值为:parent1
    任务【task2-parent2】获取到父线程的值为:parent1
    View Code

    从结果中可以看出任务2没有获取到父线程的值,而是获取到任务1的父线程的值,原因其实和case1差不多,本质原因都是因为任务1和任务2使用的是同一个线程,因此get的是同一个value

    在使用线程池时,如何在子线程中正确的获取父线程的值?

    既然问题的根源是由于使用同一个线程造成的,那么在任务执行完后,清空该线程持有的threadLocals或者inheritableThreadLocals中的value,执行其他任务时,能够重新拷贝父线程的值就好了

    如何实现?

    1.如前面的方法一,在执行任务前,备份父线程的值,任务结束后,清除该子线程的值

    扩展下,可以使用装饰器模式来装饰我们的任务。首先在任务执行备份父线程的值;在任务执行时,拷贝父线程的值到子线程;任务执行结束后,清除子线程持有的备份数据

     
    public class ExtRunnable implements Runnable {
    
        //父线程的值
        Value value = AppContext.get();
    
        private Runnable runnable;
    
        public ExtRunnable(Runnable runnable) {
            this.runnable = runnable;
        }
    
        @Override
        public void run() {
            //将父线程的值拷贝的子线程
            AppContext.set(value);
            try {
                this.runnable.run();
            } finally {
                //任务执行完后,将该子线程的值删除
                AppContext.remove();
            }
        }
    }
    View Code

    调用方式:

    executorService.submit(new ExtRunnable(new ChildThread("task1-parent1")));
    View Code

    方法三:

    阿里封装了一个工具,实现了在使用线程池等会池化复用线程的组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题

    https://github.com/alibaba/transmittable-thread-local

    官网中给出的示例代码 :

    TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
    parent.set("value-set-in-parent");
    
    Runnable task = new Task("1");
    // 额外的处理,生成修饰了的对象ttlRunnable
    Runnable ttlRunnable = TtlRunnable.get(task);
    executorService.submit(ttlRunnable);
    
    // =====================================================
    
    // Task中可以读取,值是"value-set-in-parent"
    String value = parent.get();
    View Code
  • 相关阅读:
    【DWT笔记】基于小波变换的降噪技术
    【DWT笔记】傅里叶变换与小波变换
    Matlab命令——目录操作(windows&Linux)
    【DCT笔记】DCT变换、DCT反变换、分块DCT变换
    JSOI2018 防御网络
    NOI2018 屠龙勇士
    CRT&EXCRT学习笔记
    CF662C Binary Table
    HNOI2017 礼物
    ZJOI2014 力
  • 原文地址:https://www.cnblogs.com/tracer-dhy/p/11404666.html
Copyright © 2011-2022 走看看