一、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) { "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(); publicvoidrun() { //打印主线程的值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 { publicvoidrun() { 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) { "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; } publicvoidrun() { 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,那么会影响父线程的值,因为指向的是同一个地址
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; } publicvoidrun() { //将父线程的值拷贝的子线程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();