ThreadLocal/InheritableThreadLocal
应用场景
ThreadLocal的应用非常广泛,就ThreadLocal可以实现当前线程的共享数据获取这个就太有用了。正常我们写代码都是由上一个方法传递个下一个方法来进行传递参数,假如说参数繁多,链路非常长,这个时候就会导致形参非常臃肿、传递参数非常不易。这个时候就可以使用ThreadLocal来解决这个问题。下面来分析一些使用场景:
① 登录用户上下文
第一步:用户登录HTTP协议携带JWT传给后端(可以是Header也可以是Cookie,建议Cookie,这样不用每次都塞值)
第二步:通过拦截器获取并解密JWT,拿取关键用户信息,然后从缓存中获取用户详细信息(这里如果体量不大可以直接将用户的部分核心信息塞入JWT,这样其实不用从缓存中获取了,但大多数情况还是建议从缓存中获取用户信息),创建ThreadLocal并把用户信息塞入进去
第三步:可以在业务处理逻辑中直接获取TheadLocal来拿取信息
可以看出,我们使用ThreadLocal是不是和HttpSession有点相似,我们完全可以使用ThreadLocal来替代HttpSession使用.但这里会有个问题,就是在业务层(service)如果开启一个新的线程,这个时候新的线程就会获取不到用户信息了。这个时候我们就可以使用InheritableThreadLocal
来替代ThreadLocal。
② 日志链路追踪
③ 线程之间传值
④ 灵活应用
ThreadLocal的应用场景非常多,这里只列了三种。我们可以灵活的应用ThreadLocal来解决项目的问题,但要注意ThreadLocal内存泄露的问题,用完之后一定要手动释放掉。
核心源码解析
为什么InheritableThreadLocal
可以在子线程中传递?
我们先看一下InheritableThreadLocal
的代码
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
可以看到InheritableThreadLocal
继承了ThreadLocal
类并重写了三个方法childValue
/getMap
/createMap
,那分别在什么时候会调用这些方法呢?我们可以想到在我们是在创建子线程的过程中传递的ThreadLocal值给子线程,那么是不是我们在线程中new Thread()的时候回去复制父线程的ThreadLocal给子线程呢?
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
可以看到我们在实例化Thread对象的时候会去调用init
方法,那么init方法会做哪些事情呢?我们继续往下看
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
// 可以看到这段话,如果`inheritThreadLocals`为true,并且父线程的inheritableThreadLocals不为空的时候
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 这里会创建子线程的inheritableThreadLocals,把parent.inheritableThreadLocals传递过去
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
再来看下ThreadLocal.createInheritedMap
最终调用的代码
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
// 创建当前线程的ThreadLocalEntry
table = new Entry[len];
for (int j = 0; j < len; j++) {
// 获取父线程ThreadLocal中的Entry
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
// 创建新的entry将父线程的Entry.value拷贝过来
Entry c = new Entry(key, value);
// 计算table数组位置
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
可以看到实际上是创建Thread的时候去判断线程是否创建了InheritableThreadLocal
,如果创建了InheritableThreadLocal
并且在当前线程创建了新的子线程,这个时候会把父线程的ThreadLocalMap拷贝到子线程的ThreadLocalMap中。
如果我们使用线程池父子线程的InheritableThreadLocal是否还能传递?
答案是“否”
public static void main(String[] args) throws InterruptedException {
//单一线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
//InheritableThreadLocal存储
InheritableThreadLocal<String> username = new InheritableThreadLocal<>();
for (int i = 0; i < 10; i++) {
username.set("公众号:码猿技术专栏—" + i);
Thread.sleep(3000);
CompletableFuture.runAsync(() -> System.out.println(username.get()), executorService);
}
executorService.shutdownNow();
}
可以看到我们在父线程中修改了ThreadLocal变量username
的值,但实际上子线程中的username
并没有被修改。在上面我们分析了InheritableThreadLocal
的源码知道只有在创建线程的时候ThreadLocal的值才会进行一次复制,后续的修改是不会同步到子线程中去的。那这个问题该怎么解决呢?
这个时候我们可以使用
TransmittableThreadLocal
@Test
public void test1() throws InterruptedException {
//单一线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
//需要使用TtlExecutors对线程池包装一下
executorService= TtlExecutors.getTtlExecutorService(executorService);
//TransmittableThreadLocal创建
TransmittableThreadLocal<String> username = new TransmittableThreadLocal<>();
for (int i = 0; i < 10; i++) {
username.set("公众号:码猿技术专栏—" + i);
Thread.sleep(3000);
CompletableFuture.runAsync(() -> System.out.println(username.get()), executorService);
}
executorService.shutdownNow();
}
那么原理是什么呢?在本文最后一节会有介绍。
为什么ThreadLocal会内存泄露
强引用与弱引用
强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
GC回收机制-如何找到需要回收的对象
JVM如何找到需要回收的对象,方式有两种:
引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收,
可达性分析法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。
引用计数法,可能会出现A 引用了 B,B 又引用了 A,这时候就算他们都不再使用了,但因为相互引用 计数器=1 永远无法被回收。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。这些对象之间的引用关系如下:
实心箭头表示强引用,空心箭头表示弱引用
ThreadLocal 内存泄漏的原因
从上图中可以看出,hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。
但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
ThreadLocal正确的使用方法
每次使用完ThreadLocal都调用它的remove()方法清除数据
将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
为什么建议把ThreadLocal声明为static?
这样就保留了ThreadLocal为强引用了,因为他是GCRoot对象,永不可能回收,key(ThreadLocal对象)是不是弱引用已经无所谓了。因为ThreadLocal是静态变量,所以只存在一个ThreadLocal对象,即使是线程池的线程(假设不发生异常,永远存活),这个ThreadLocal对象也只会占用ThreadLocalMap一个Entry的坑位,换句话说,及时我不回收他,我声明的这个对象在一个线程里只占一个Entry的坑位,永不回收问题也不是很大。对比局部变量ThreadLocal,不回收,每跑一次run方法可能就生成一个Entry,只要不调用remove,内存就会一直泄漏下去。
为什么ThreadLocalMap中的Entry的key要声明为WeakReference?
在线程池中,一个线程会不断的从队列中拿去任务执行(不发生异常的话),这个线程是会一直存活的,换句话说他的ThreadLocalMap一直都在。
如果此时ThreadLocal作为run方法里的局部变量,且ThreadLocalMap的key不声明为弱引用(则为硬引用)。run方法跑完,threadLocal被回收,但是线程不会结束(因为是线程池的线程),ThreadLocalMap不会被回收,对应上一步的生成的Entry还是保留在ThreadLocalMap的数组中,只要Thread不死(ThreadLocalMap就还在),GC就无法回收那个Entry(key,value),然后key不为null,已经没办法判断是不是回收的了。
还是讨论ThreadLocal作为run方法里的局部变量,且ThreadLocalMap的key声明为弱引用。run方法跑完,threadLocal被回收,但是线程不会结束(因为是线程池的线程),ThreadLocalMap不会被回收,对应上一步的生成的Entry还是保留在ThreadLocalMap的数组中。等待某次GC后,Entry的key被回收,变成Entry(null,value),在remove源码中,有对key==null的Entry进行value=null的help GC的操作。在某次调用remove还能再补救一下,相比为硬引用,已经没办法补救了,已经识别不出来哪些是正在需要GC的了。
总结来说,在ThreadLocal为局部变量的时候,不声明为WeekReference,忘记remove了,然后ThreadLocal被回收后,再也没办法确认ThreadLocalMap这个认为是用完的Entry(key,value)节点是不是可以GC了。而弱引用的Entry的key,某次GC后,无用的Entry都被标记为(null, value),此时就能识别出来是泄露的内存。
TransmittableThreadLocal
应用场景
首先,TTL是用来解决ITL解决不了的问题而诞生的,所以TTL一定是支持父线程的本地变量传递给子线程这种基本操作的,ITL也可以做到,但是前面有讲过,ITL在线程池的模式下,就没办法再正确传递了,所以TTL做出的改进就是即便是在线程池模式下,也可以很好的将父线程本地变量传递下去。
源码解析
本来是想自己写的,发现之前看的一篇文章写的太好了,我没必要再写一篇我重复的文章了。所以这里直接贴链接:
TransmittableThreadLocal的使用及原理解析