今天聊一聊并发编程中经常遇到也是面试时容易被为难的一个题目,线程间局部变量的传递问题。
相信对并发编程有一定了解的同学已经想到了大名鼎鼎的 ThreadLocal 了,是的,线程内部就是通过 inheritableThreadLocals
实现了父子线程间局部变量的传递。
JDK 8
一、父子线程间局部变量参数传递的方式 ThreadLocal
首先我们先看一段代码。
public class ThreadLocalTest implements Runnable{
private static final InheritableThreadLocal<String> MAIN_THREAD_LOCAL = new InheritableThreadLocal<>();
@SneakyThrows
@Override
public void run() {
System.out.println("threadlocal 默认值:"+ThreadLocalTest.MAIN_THREAD_LOCAL.get());
MAIN_THREAD_LOCAL.set("child thread value :"+Thread.currentThread().getName());
System.out.println("threadlocal 设置子线程值之后:"+ThreadLocalTest.MAIN_THREAD_LOCAL.get());
}
public String get(){
return MAIN_THREAD_LOCAL.get();
}
public void clean(){
MAIN_THREAD_LOCAL.remove();
}
public static void main(String[] args) {
ThreadLocalTest threadLocalTest = new ThreadLocalTest();
MAIN_THREAD_LOCAL.set("父线程的值 set 111");
System.out.println("启动:"+threadLocalTest.get());
for (int i = 0; i < 3; i++) {
new Thread(threadLocalTest).start();
// ThreadUtil.execAsync(threadLocalTest);
}
System.out.println("结束:"+threadLocalTest.get());
}
}
在上面的这段代码中,我们就做了三个事情。
- 设置父线程中定义
ThreadLocal
的值。 - 在子线程中打印父线程中
ThreadLocal
的值。 - 启动多个子线程
大家可以先猜一下这段代码的运行结果。
二、子线程可以继承父线程局部变量的值吗
首先我们先说下答案,是可以继承
的。上面代码的执行结果如下。
启动:父线程的值 set 111
结束:父线程的值 set 111
threadlocal 默认值:父线程的值 set 111
threadlocal 设置子线程值之后:child thread value :Thread-1
threadlocal 默认值:父线程的值 set 111
threadlocal 默认值:父线程的值 set 111
threadlocal 设置子线程值之后:child thread value :Thread-2
threadlocal 设置子线程值之后:child thread value :Thread-0
在上面的代码中,我们的子线程优先打印了父线程中ThreadLocal
的值,然后重新设置该值,再次读取。得出结论就是子线程可以通过ThreadLocal
继承父线程的值,并且子线程自己内容再次重新设置不影响父线程的值。
三、父子线程局部变量传值的原理
难道一句简单的ThreadLocal
就可以让我们对这个问题停止探索吗?那么线程内部是如何通过ThreadLocal
进行传值的呢?
3.1、new thread
在上面代码中,启动子线程的方式是new Thread(threadLocalTest).start();
,所以秘密一定就在这一行代码里面。源码之下无秘密,我们一起来看下。
首先进入new Thread()
的内部。
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) {}
init
方法有个参数inheritThreadLocals
,boolean
类型的,如果为true
,且可继承的线程局部变量不为空就继承。
现在我们只需要顺着inheritThreadLocals
这个参数去找就可以了,在Thread
的418行,有这样一行代码。(代码行数可能因版本而位置不同)
可以看到是直接对当前线程的inheritableThreadLocals
直接进行的赋值操作,而值是通过ThreadLocal.createInheritedMap
获取的,下面我们看下这个createInheritedMap
方法做了哪些操作?
createInheritedMap
方法是ThredLocal
内部的方法,接收传递父线程的ThreadLocalMap
为参数,该方法只做了一个事情,就是new
了一个新的ThreadLocalMap
。
跟进到new ThreadLocalMap(parentMap)
方法内部,其实是把传进的值,一个个的遍历进行赋值到当前线程中。
对于图中标记的第二个地方,childValue
调用的是InheritableThreadLocal#childValue
,该方法内也只做了一件事,就是返回传进来的值。
小结
父子线程之所以能传参,是因为我们使用了InheritableThreadLocal
,这样在new Thread()
时,就会进入到给子线程赋值父线程inheritableThreadLocals
的逻辑中去。
扩展
有的同学会说了,我用 ThreadLocal.withInitial
创建的,怎么走到线程的if (inheritThreadLocals && parent.inheritableThreadLocals != null)
判断时,没有进去呢,上面不是说是在这判断然后对子线程进行赋值的吗?
在这简单说一下哈,大家在写代码时,或者再用第三方框架时,源码中的注释
一定要看仔细的,很多的细节都在注释中标注清楚了。
public static ThreadLocal<String> MAIN_THREAD_LOCAL = ThreadLocal.withInitial(() -> "父线程的值 withInitial 111");
在上面的代码中,我们进行了ThreadLocal的初始化赋值,然后看下withInitial
方法。
所以是当调用get
方法时,才会触发赋值的操作,那么我们看下get
方法。
如果当前线程的局部变量没有值,返回初始方法初始化的值。
所以对于我们来说就是SuppliedThreadLocal#initialValue
返回的值。
3.2、线程池
刚才我们是通过new Thread()
启动的子线程,可是工作中基本都是通过线程池的方式执行任务的啊,那还生效吗?
答案是生效
。
我们使用hutool
工具中的ThreadUtil.execAsync(threadLocalTest);
进行测试。
直接说结论把,感兴趣的同学可以自行修改一下代码中的子线程启动方式。
先画个流程图,大家可以跟着代码走一下。
当使用线程池时,底层原理还是线程池中放入任务的逻辑,当放入线程池之后,会在AbstractExecutorService#submit()
方法中执行execute
方法,最终执行在ThreadPoolExecutor#execute()
,在这里,就是把任务丢入线程池工作的逻辑,其中有个方法addWorker
,该方法中有一行new Worker()
,而在该Worker方法的内部,其实就是new Thread()
,到了这,就与上面所说的一样了,到了判断inheritableThreadLocals
的时候了。
四、如何解决内存泄漏
使用ThreadLocal
的应用场景有很多,父子线程传参数的场景也有不少,但是有一个很关键的点内存泄漏
是需要重视的。解决ThreadLocal
内存泄漏的方式也很简单,就是在使用完成之后调用一下remove
。
对于上面的代码示例,就是调用我们的clean方法。
public void clean(){ MAIN_THREAD_LOCAL.remove(); }
remove
的代码如下,取值不为null
时,执行删除逻辑。
五、总结
我们通过一个示例,验证了父子线程间可以通过ThreadLocal
进行传递,测试了不同方式初始化ThreadLocal
,并对比了new Thread()
与线程池启动的区别。
其实殊途同归,线程池最后调用的还是Thread
里面的方法。唯一需要注意的就是通过ThreadLocal.withInitial
初始化是在get
时赋值的,不过这个应该也不重要,了解一下就好,应该也没有面试官会这么抠这个问题吧。
最重要的就是,不管用什么方式,一定要在使用完成之后进行remove
。
大家可以在评论区交流一下父子间线程传递参数的场景有哪些!
点击原文 《醉鱼Java》,回复面试,获取2024面试资料。
如果这篇文章对您有所帮助或者启发,帮忙点个关注叭,您的支持是我坚持写作的最大动力。
求一键三连:点赞、收藏、关注。
谢谢支持哟 ( ^__^ )。