线程 - 父线程与子线程传值问题

简介: 线程 - 父线程与子线程传值问题

一、ThreadLocal回顾

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

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

看下ThreadLocal的get方法

publicTget() {
//当前线程Threadt=Thread.currentThread();
//获取当前线程的ThreadLocalMap对象ThreadLocalMapmap=getMap(t);
if (map!=null) {
//获取该ThreadLocal对象的valueThreadLocalMap.Entrye=map.getEntry(this);
if (e!=null) {
@SuppressWarnings("unchecked")
Tresult= (T)e.value;
returnresult;
        }
    }
//设置初始值returnsetInitialValue();
}
//获取当前线程的ThreadLocalMap对象ThreadLocalMapgetMap(Threadt) {
returnt.threadLocals;
}

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

publicvoidset(Tvalue) {
//获取当前线程Threadt=Thread.currentThread();
//获取当前线程的ThreadLocalMap对象ThreadLocalMapmap=getMap(t);
//如果ThreadLocalMap对象存在,则直接设置key(ThreadLocal对象),value;否则创建ThreadLocalMap对象,并设置key,valueif (map!=null)
map.set(this, value);
elsecreateMap(t, value);
}
voidcreateMap(Threadt, TfirstValue) {
t.threadLocals=newThreadLocalMap(this, firstValue);
}

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

 

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

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

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

staticThreadLocal<String>threadLocal=newThreadLocal<>();
publicstaticvoidmain(String[] args) {
threadLocal.set("dhytest");
newThread(()->{
System.out.println("子线程获取到的值:"+threadLocal.get());
    }).start();
System.out.println("父线程获取到的值:"+threadLocal.get());
}

运行结果

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

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

方法一

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

staticThreadLocal<String>threadLocal=newThreadLocal<>();
privatevoidtest() throwsInterruptedException {
Thread.currentThread().setName("main-thread");
//主线程设置一个值threadLocal.set(newValue("dhyTest"));
//运行子线程ThreadchildThread=newThread(newParentChildTransferValue2.ChildThread(), "child-thread");
childThread.start();
//主线成等待子线程运行完,以便观察主线中设置的值是否被子线程成功修改childThread.join();
System.out.println("父线程获取到的最终的值:"+threadLocal.get());
}
classChildThreadimplementsRunnable {
//获取主线程中设置的值Valuevalue=threadLocal.get();
@Overridepublicvoidrun() {
//打印主线程的值System.out.println("原父线程的值: "+value);
//如果启用了线程(调用start方法),调用get方法是获取不到值的ValuenullValue=threadLocal.get();
System.out.println("子线程中直接调用get方法获取父线程的值,value:"+nullValue);
//获取到父线程的值,并进行更改value.setData(value.getData() +"---子线程对父线程的值做了修改" );
    }
}

运行结果

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


方法二

使用 InheritableThreadLocal

staticInheritableThreadLocal<Value>threadLocal=newInheritableThreadLocal<>();
privatevoidtest() throwsInterruptedException {
Thread.currentThread().setName("main-thread");
//主线程设置一个值threadLocal.set(newValue("dhyTest"));
//运行子线程ThreadchildThread=newThread(newChildThread(), "child-thread");
childThread.start();
//主线成等待子线程运行完,以便观察主线中设置的值是否被子线程成功修改childThread.join();
System.out.println("父线程获取到的最终的值:"+threadLocal.get());
}
classChildThreadimplementsRunnable {
@Overridepublicvoidrun() {
Valuevalue=threadLocal.get();
System.out.println("子线程中直接调用get方法获取父线程的值,value:"+value);
value.setData(value.getData() +"---子线程对父线程的值做了修改");
    }
}

运行结果

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

InheritableThreadLocal分析

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

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

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

publicclassInheritableThreadLocal<T>extendsThreadLocal<T> {
protectedTchildValue(TparentValue) {
returnparentValue;
    }
ThreadLocalMapgetMap(Threadt) {
returnt.inheritableThreadLocals;
    }
voidcreateMap(Threadt, TfirstValue) {
t.inheritableThreadLocals=newThreadLocalMap(this, firstValue);
    }
}

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

publicThread(Runnabletarget) {
init(null, target, "Thread-"+nextThreadNum(), 0);
}
privatevoidinit(ThreadGroupg, Runnabletarget, Stringname,
longstackSize) {
init(g, target, name, stackSize, null, true);
}
privatevoidinit(ThreadGroupg, Runnabletarget, Stringname,
longstackSize, AccessControlContextacc,
booleaninheritThreadLocals) {
if (name==null) {
thrownewNullPointerException("name cannot be null");
    }
this.name=name;
//这里获取了父线程Threadparent=currentThread();
SecurityManagersecurity=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();
}

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

staticThreadLocalMapcreateInheritedMap(ThreadLocalMapparentMap) {
returnnewThreadLocalMap(parentMap);
}
privateThreadLocalMap(ThreadLocalMapparentMap) {
Entry[] parentTable=parentMap.table;
intlen=parentTable.length;
setThreshold(len);
table=newEntry[len];
for (intj=0; j<len; j++) {
Entrye=parentTable[j];
if (e!=null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object>key= (ThreadLocal<Object>) e.get();
if (key!=null) {
Objectvalue=key.childValue(e.value);
Entryc=newEntry(key, value);
inth=key.threadLocalHashCode& (len-1);
//如果发生hash碰撞,那么槽位后移while (table[h] !=null)
h=nextIndex(h, len);
table[h] =c;
size++;
            }
        }
    }
}

InheritableThreadLocal存在的问题

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

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

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

staticInheritableThreadLocal<Value>threadLocal=newInheritableThreadLocal<>();
staticExecutorServiceexecutorService=Executors.newSingleThreadExecutor();
privatevoidtest() throwsInterruptedException, ExecutionException {
threadLocal.set(newValue("dhytest"));
Future<?>task1=executorService.submit((newChildThread("task1")));
task1.get();
Future<?>task2=executorService.submit(newChildThread("task2"));
task2.get();
System.out.println("父线程的值:"+threadLocal.get());
}
classChildThreadimplementsRunnable {
privateStringtaskName;
ChildThread(StringtaskName) {
this.taskName=taskName;
    }
@Overridepublicvoidrun() {
Valuevalue=threadLocal.get();
System.out.println("任务【"+taskName+"】获取到父线程的值为:"+value);
threadLocal.set(newValue("值被任务【"+taskName+"】修改啦"));
//如果使用下面的代码,那么父线程的值是会被改变的//value.set(new Value("父线程的值也被修改啦,因为是引用传递"));    }
}

运行结果

1. 任务【task1】获取到父线程的值为:dhytest
2. 任务【task2】获取到父线程的值为:值被任务【task1】修改啦
3. 父线程的值:dhytest

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

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

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

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

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

image.png

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

privatevoidtest() throwsInterruptedException, ExecutionException {
//父线程1Threadparent1=newThread(() -> {
threadLocal.set(newValue("parent1"));
Future<?>task1=executorService.submit((newChildThread("task1-parent1")));
try {
task1.get();
        } catch (InterruptedExceptione) {
e.printStackTrace();
        } catch (ExecutionExceptione) {
e.printStackTrace();
        }
    });
parent1.start();
parent1.join();
//父线程2Threadparent2=newThread(() -> {
threadLocal.set(newValue("parent2"));
Future<?>task2=executorService.submit((newChildThread("task2-parent2")));
try {
task2.get();
        } catch (InterruptedExceptione) {
e.printStackTrace();
        } catch (ExecutionExceptione) {
e.printStackTrace();
        }
    });
parent2.start();
}

运行结果

1. 任务【task1-parent1】获取到父线程的值为:parent1
2. 任务【task2-parent2】获取到父线程的值为:parent1

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


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

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

如何实现?

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

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

publicclassExtRunnableimplementsRunnable {
//父线程的值Valuevalue=AppContext.get();
privateRunnablerunnable;
publicExtRunnable(Runnablerunnable) {
this.runnable=runnable;
    }
@Overridepublicvoidrun() {
//将父线程的值拷贝的子线程AppContext.set(value);
try {
this.runnable.run();
        } finally {
//任务执行完后,将该子线程的值删除AppContext.remove();
        }
    }
}

调用方式

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

方法三

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

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

官网中给出的示例代码

TransmittableThreadLocal<String>parent=newTransmittableThreadLocal<String>();
parent.set("value-set-in-parent");
Runnabletask=newTask("1");
// 额外的处理,生成修饰了的对象ttlRunnableRunnablettlRunnable=TtlRunnable.get(task);
executorService.submit(ttlRunnable);
// =====================================================// Task中可以读取,值是"value-set-in-parent"Stringvalue=parent.get();
目录
相关文章
|
Java 测试技术 数据处理
Java多线程父线程向子线程传值解决方案 2
Java多线程父线程向子线程传值解决方案
311 0
|
Java
Java多线程父线程向子线程传值解决方案 1
Java多线程父线程向子线程传值解决方案
519 0
|
Python
python多线程----------主线程,子线程,任务讲解----拿下就是胜利
python多线程----------主线程,子线程,任务讲解----拿下就是胜利
182 0
京东一面:子线程如何获取父线程 ThreadLocal 的值?我蒙了。。。
京东一面:子线程如何获取父线程 ThreadLocal 的值?我蒙了。。。
449 0
京东一面:子线程如何获取父线程 ThreadLocal 的值?我蒙了。。。
京东一面:子线程如何获取父线程ThreadLocal的值
京东一面:子线程如何获取父线程ThreadLocal的值
京东一面:子线程如何获取父线程ThreadLocal的值
|
存储 Java
子线程无法拿到父线程的变量怎么办?|Java 开发实战
数据在哪个线程存储,就要从哪个线程读取,子线程是读取不到的
412 0
【EventBus】事件通信框架 ( 发送事件 | 判断发布线程是否是主线程 | 子线程切换主线程 | 主线程切换子线程 )(二)
【EventBus】事件通信框架 ( 发送事件 | 判断发布线程是否是主线程 | 子线程切换主线程 | 主线程切换子线程 )(二)
183 0
|
Android开发
【EventBus】事件通信框架 ( 发送事件 | 判断发布线程是否是主线程 | 子线程切换主线程 | 主线程切换子线程 )(一)
【EventBus】事件通信框架 ( 发送事件 | 判断发布线程是否是主线程 | 子线程切换主线程 | 主线程切换子线程 )(一)
226 0
|
2月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
139 0
|
2月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。