原创 吴就业 114 0 2021-02-18
本文为博主原创文章,未经博主允许不得转载。
本文链接:https://wujiuye.com/article/47dca23a8b7a4b94a2f796c590eda2d0
作者:吴就业
链接:https://wujiuye.com/article/47dca23a8b7a4b94a2f796c590eda2d0
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。
本篇文章写于2021年02月18日,从公众号|掘金|CSDN手工同步过来(博客搬家),本篇为原创文章。
由于上次主要分析如何解决异步获取不到Session
问题,所以没有展开分析留下的那个思考题:使用InheritableThreadLocal
传递Session
,为什么说使用线程池不一定能获取到Session
,而不是一定获取不到?《替换Shiro框架后,上线就Bug了,异步线程获取不到Session》
在Java
中,一个Java
线程就是一个操作系统线程,创建一个线程需要通过new Thread
创建,由JVM
为Thread
绑定操作系统线程,即便是使用线程池,也需要通过new Thread
创建线程。
Thread
类有两个ThreadLocal
字段:
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
InheritableThreadLocal
是ThreadLocal
的子类,本质上就是一个ThreadLocal
。
在Thread
类中,threadLocals
与inheritableThreadLocals
都是线程对象私有的,只能通过当前线程对象写入和获取数据,只是Thread
会将写入inheritableThreadLocals
的数据传递给子线程的inheritableThreadLocals
。
当我们往ThreadLocal
或者InheritableThreadLocal
写入数据时,写入过程为:
ThreadLocal
或者InheritableThreadLocal
先调用Thread#currentThread
静态方法获取当前线程的Thread
对象;Thread
对象的threadLocals
或者inheritableThreadLocals
;ThreadLocal
或者InheritableThreadLocal
对象作为key
,将数据写入到当前Thread
对象的threadLocals
或者inheritableThreadLocals
字段中。因此,Thread
的threadLocals
与inheritableThreadLocals
的key
是ThreadLocal
或者InheritableThreadLocal
实例,value
是写入的数据。关于threadLocals
我在前面一篇《反向理解ThreadLocal,或许这样更容易理解》已经详细介绍过了,本篇重点分析inheritableThreadLocals
是如何传递给子线程的。
默认情况下,当我们使用new Thread()
创建一个线程时,在Thread
的构造方法中会通过Thread#currentThread
获取当前线程,将当前线程作为新创建线程的父线程,所以就有了父子线程关系。
无论使用哪个重载的构造方法创建Thread
,都会在构造方法中调用init
方法完成初始化为Thread
字段赋值,而init
方法中有这样一段代码:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
......
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
......
}
在init
方法中,由于inheritThreadLocals
参数默认为true
,所以只要父线程的inheritableThreadLocals
字段不为空,就copy
一份父线程的inheritableThreadLocals
给当前创建的线程对象,这就实现了将父线程的inheritableThreadLocals
存储的数据传递给子线程。
使用InheritableThreadLocal
我们不得不考虑的问题:内存泄漏。
ThreadLocal.ThreadLocalMap
使用数组存储元素,与HashMap
不同,它通过开放定址法解决hash
冲突,不存在链表,通过动态扩容数组可无限存储元素,数组元素的类型为Entry
。
当我们往ThreadLocal.ThreadLocalMap
写入一个key-value
时,ThreadLocalMap
把key
和value
包装成一个Entry
,并通过key
的hashcode
值计算索引值,将Entry
放到数组中。
ThreadLocal.ThreadLocalMap.Entry
类的源码如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
虽然key
为弱引用的ThreadLocal
,当ThreadLocal
释放时,Entry
的key
变为null
,但由于value
还在,如果Thread
不释放,那么Entry
也就不会被垃圾收集器回收。
但如果线程是临时创建的,在方法中创建且没有被其它地方引用,当线程执行完成时就会被JVM
销毁,在线程实际退出之前由JVM
调用线程的exit
方法给线程对象完成清理。exit
方法部分源码如下。
private void exit() {
......
threadLocals = null;
inheritableThreadLocals = null;
......
}
因此,只要Thread
对象的exit
方法被调用,就不会存在内存泄漏问题。只要线程用完就销毁,那么使用InheritableThreadLocal
,在子线程中不需要调用InheritableThreadLocal
的remove
方法也不会存在内存泄漏的可能。
比如我们在项目中使用InheritableThreadLocal
实现将Session
传递给子线程:
@GetMapping("/test")
public SsoUser test() {
// 获取登录用户
SsoUser ssoUser = SsoUserManager.curLoggedUser();
System.out.println(ssoUser.getUserCode());
// 支持子线程传递
new Thread(() -> {
try {
Thread.sleep(100);
SsoUser ssoUser2 = SsoUserManager.curLoggedUser();
System.out.println(ssoUser2.getUserCode());
} catch (InterruptedException e) {
}
}).start();
return ssoUser;
}
在此案例中,由于子线程只是临时创建的,所以我们不需要在子线程中调用InheritableThreadLocal
的remove
方法,只需要在父线程调用一次remove
方法,因为tomcat
的work
线程是不会在一次请求结束后就销毁的。
现在我们已经知道了InheritableThreadLocal
是如何实现将数据传递给子线程的,思考题的答案也就有了一半:由于InheritableThreadLocal
只能将线程上下文传递给当前线程创建的子线程,所以只有线程池中的线程是由当前线程创建的才能够传递。
但要知道另一半答案我们还需要从线程池中寻找。
使用不同参数构建的线程池不同,常见的有单线程的线程池、只有固定数量核心线程的线程池、有固定数量核心线程和非核心线程的线程池、只有非核心线程的线程池。
线程池的几个构造参数说明如下:
corePoolSize
:核心线程数,不会被释放的线程数量(设置allowCoreThreadTimeOut
为ture
时例外);maximumPoolSize
:线程池的最大线程数,等于核心线程与非核心线程的数量总和;keepAliveTime
:非核心线程最大空闲等待时间,在指定空闲时间后如果还没有任务则释放该线程;workQueue
:任务队列,当核心线程数用完时,任务被放入队列。一、线程池是临时线程池
如果线程池是在当前线程创建的,且任务都是由当前线程提交的,线程池用完就消毁了,那么不管是哪种线程池,池中的线程都是由当前线程所创建,在这种场景下,InheritableThreadLocal
能够将Context
传给给线程池中的任一线程。
二、线程池是全局线程池
如果线程池是全局线程池:
keepAliveTime
等于0
:线程都是用到才创建,且由于keepAliveTime
等于0
,线程用完就释放了,在这种场景下,相当于是由当前线程创建子线程执行任务,因此能够实现透传;InheritableThreadLocal
的数据的,后面提交的任务就不知道会被哪个核心线程拉取执行了;因此,如果线程池是全局线程池,那么无论是哪个情况,都不建议使用InheritableThreadLocal
。
声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。
面向文件编程的重要性;简单文件读写;随机访问文件读写;NIO文件读写-FileChannel;使用MappedByteBuffer读写文件。
订阅
订阅新文章发布通知吧,不错过精彩内容!
输入邮箱,提交后我们会给您发送一封邮件,您需点击邮件中的链接完成订阅设置。