一、ThreadLocal回顾

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

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

看下ThreadLocal的get方法

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

publicT 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;returnresult;

}

}//设置初始值

returnsetInitialValue();

}//获取当前线程的ThreadLocalMap对象

ThreadLocalMap getMap(Thread t) {returnt.threadLocals;

}

View Code

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

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

public voidset(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);elsecreateMap(t, value);

}voidcreateMap(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

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

static ThreadLocal threadLocal = new ThreadLocal<>();public static voidmain(String[] args) {

threadLocal.set("dhytest");new Thread(()->{

System.out.println("子线程获取到的值:" +threadLocal.get());

}).start();

System.out.println("父线程获取到的值:" +threadLocal.get());

}

View Code

运行结果:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

父线程获取到的值:dhytest

子线程获取到的值:null

View Code

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

方法一:

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

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

static ThreadLocal threadLocal = new ThreadLocal<>();private void test() throwsInterruptedException {

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 implementsRunnable {//获取主线程中设置的值

Value value =threadLocal.get();

@Overridepublic voidrun() {//打印主线程的值

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

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

static InheritableThreadLocal threadLocal = new InheritableThreadLocal<>();private void test() throwsInterruptedException {

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 implementsRunnable {

@Overridepublic voidrun() {

Value value=threadLocal.get();

System.out.println("子线程中直接调用get方法获取父线程的值,value:" +value);

value.setData(value.getData()+ "---子线程对父线程的值做了修改");

}

}

View Code

运行结果:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

子线程中直接调用get方法获取父线程的值,value:dhyTest

父线程获取到的最终的值:dhyTest---子线程对父线程的值做了修改

View Code

InheritableThreadLocal分析

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

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

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

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

public class InheritableThreadLocal extends ThreadLocal{protectedT childValue(T parentValue) {returnparentValue;

}

ThreadLocalMap getMap(Thread t) {returnt.inheritableThreadLocals;

}voidcreateMap(Thread t, T firstValue) {

t.inheritableThreadLocals= new ThreadLocalMap(this, firstValue);

}

}

View Code

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

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

publicThread(Runnable target) {

init(null, target, "Thread-" + nextThreadNum(), 0);

}private voidinit(ThreadGroup g, Runnable target, String name,longstackSize) {

init(g, target, name, stackSize,null, true);

}private voidinit(ThreadGroup g, Runnable target, String name,longstackSize, AccessControlContext acc,booleaninheritThreadLocals) {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

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

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

staticThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return newThreadLocalMap(parentMap);

}privateThreadLocalMap(ThreadLocalMap parentMap) {

Entry[] parentTable=parentMap.table;int len =parentTable.length;

setThreshold(len);

table= newEntry[len];for (int j = 0; j < len; j++) {

Entry e=parentTable[j];if (e != null) {

@SuppressWarnings("unchecked")

ThreadLocal key = (ThreadLocal) e.get();if (key != null) {

Object value=key.childValue(e.value);

Entry c= newEntry(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:模拟使用线程池情况下(为了便于测试不同任务有同一个线程处理的场景,使用单线程),两个任务由同一个线程处理。

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

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

static InheritableThreadLocal threadLocal = new InheritableThreadLocal<>();static ExecutorService executorService =Executors.newSingleThreadExecutor();private void test() throwsInterruptedException, 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 implementsRunnable {privateString taskName;

ChildThread(String taskName) {this.taskName =taskName;

}

@Overridepublic voidrun() {

Value value=threadLocal.get();

System.out.println("任务【" + taskName + "】获取到父线程的值为:" +value);

threadLocal.set(new Value("值被任务【" + taskName + "】修改啦"));//如果使用下面的代码,那么父线程的值是会被改变的//value.set(new Value("父线程的值也被修改啦,因为是引用传递"));

}

}

View Code

运行结果:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

任务【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,那么会影响父线程的值,因为指向的是同一个地址

b6caa09a9347caacb009d6724ce00612.png

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

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

private void test() throwsInterruptedException, 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

运行结果:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

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

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

View Code

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

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

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

如何实现?

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

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

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

public class ExtRunnable implementsRunnable {//父线程的值

Value value =AppContext.get();privateRunnable runnable;publicExtRunnable(Runnable runnable) {this.runnable =runnable;

}

@Overridepublic voidrun() {//将父线程的值拷贝的子线程

AppContext.set(value);try{this.runnable.run();

}finally{//任务执行完后,将该子线程的值删除

AppContext.remove();

}

}

}

View Code

调用方式:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

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

View Code

方法三:

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

官网中给出的示例代码 :

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

TransmittableThreadLocal parent = new TransmittableThreadLocal();

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

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐