(Inheritable)ThreadLocal源码分析以及内存泄漏等问题

2023年 9月 28日 40.4k 0

ThreadLocal:线程本地变量

ThreadLocal:线程本地变量,可以起到线程隔离作用,即每个线程访问自己的变量,不再是共享一个变量。

ThreadLocal利用泛型来封装「任意的自定义类」,我们定义ThreadLocal封装的任意的自定义类为「资源」,ThreadLocal就是在「资源」的基础上做了一层封装

应用场景

最常见的「资源」是数据库连接、simpleDateFormat等;

要么是线程独享的对象,要么是便于在线程生命周期的任意时间点方便的拿到。

在业务代码中,通常会通过定义一个全局的类对ThreadLocal进行封装,从而使得web服务器的请求线程可以轻松的拿到关键的业务数据,比如userId

快速使用

public class ThreadLocaDemo {
    private static ThreadLocal localVar = new ThreadLocal();
     new Thread(()->{
                localVar.set("local_A");
                System.out.println(localVar.get());
        }).start();
}  

三个API:增删改查

  • set(T):增/改
  • remove:删
  • get:查

ThreadLocal实现原理

既然ThreadLocal是对「资源」的封装,那我们第一个要解决的问题就是:ThreadLocal如何存储资源? 你会发现ThreadLocal内部没有任何字段保存了资源,那么资源去哪里了?

  • JDK8之前,ThreadLocal维护ThreadLocalMap,以Thread为key存对应的数据
  • 现在Thread维护ThreadLocalMap,以ThreadLocal为key存对应的数据

下面基于JDK8源码分析

线程存储ThreadLocalMap

ThreadLocalMap是一张哈希表。key是ThreadLocal,value是ThreadLocal封装的「资源」。

ThreadLocalMap所有方法都是private的,没有向外暴露任何方法,因此如果仅仅想知道ThreadLocal的工作原理,我们完全不需要关注它的实现原理。

我们暂时可以把ThreadLocalMap理解成类HashMap,提供了类似哈希表数据结构的get/set/remove等功能,把ThreadLocalMap当成黑盒来看,关于ThreadLocalMap这样设计的原因我们后面再深入分析,先来看看ThreadLocal最核心的功能如何实现。

读写:转化为对Map的操作

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

拿到线程的ThreadLocalMap,调用getEntry方法,如果value为null设置为初始值并返回

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

同样转化为对线程的ThreadLocalMap的操作,如果没有Map就初始化。

还支持remove操作,也是一样的,就不贴源码了

自定义初始值:重写initialValue

get方法如果为null,ThreadLocal会返回一个可以自定义的初始值,自定义方法是initialValue

private T setInitialValue() {
    T value = initialValue();
    // 省略维护Map
    return value;
}
   protected T initialValue() {
        return null;
    }

我们可以通过重写initialValue方法从而设置ThreadLocal的资源的初始值

new ThreadLocal() {
        @Override
        protected MyResource initialValue() {
            // 自定义初始值
            return new MyResource();
        }
    };

绕过了ThreadLocalMap,实际上对ThreadLocal的实现原理就已经讲完了,但是:

但凡稍微背过点八股文的都知道:ThreadLocal是有「内存泄漏」的风险的。即ThreadLocalMap这个黑盒到底做了些什么事情,又有什么风险,要如何解决呢?

ThreadLocal的风险

1、脏数据

线程池一个很大的作用是「线程复用」,而线程复用,就会导致复用的线程共享同一个ThreadLocalMap,因此如果没有及时remove ThreadLocal,直接调用get而期望拿到initial value,很可能会拿到上个线程的ThreadLocal值,这种风险是致命的。

要解决也不难:

  • 在线程执行完毕后,结束前,remove所有的ThreadLocal
  • 保证所有线程使用ThreadLocal前,先set从而保证是自己的value

但第二种做法会导致initial value的失效,因此推荐remove方法

2、内存泄漏问题

我们先来看Map的键值对。

Entry:Map的键值对

static class Entry extends WeakReference
private T referent;

我们发现:Entry的key,也就是ThreadLocal,被设计成了弱引用。

弱引用:对象值得被回收

为什么key是弱引用

解决「内存泄露」问题。

内存泄露:对象没有被引用,已经是个无用的对象了,但是GC无法将该对象回收。

存在这样一条引用链:Thread -> ThreadLocalMap -> Entry -> ThreadLocal

因此,如果是强引用,即使没有任何引用指向ThreadLocal,只要线程不死亡,ThreadLocal 永远都不会死亡。因此,将key设计为弱引用,就可以解决这个问题。

但是Entry的value还是强引用,还是会发生「内存泄露」。即key为null,value还在。

value的内存泄漏

上图来自Richard_Yi,很好的阐述了value的内存泄漏问题

因此使用完ThreadLocal,需要手动调用remove()回收。

ThreadLocal在执行get set remove方法都会尽量把ThreadLocal为null的entry回收掉,这是ThreadLocal尽力解决「内存泄漏」问题,但是无法彻底解决,还是要手动remove

题外话:ThreadLocal如何作为key

ThreadLocal类有个类变量:

private static final int HASH_INCREMENT = 0x61c88647;

所有ThreadLocal的实例对象有个threadLocalHashCode

private final int threadLocalHashCode = nextHashCode();

next也体现了ThreadLocal类的每个实例对象的threadLocalHashCode都尽量不相同,因此:与HashMap不同,HashMap利用HashCode方法获取HashCode,ThreadLocal额外维护了自己的threadLocalHashCode。

子线程获取父线程的ThreadLocal

很多场景下,ThreadLocal需要共享,子线程需要访问父线程的ThreadLocal,Java提供了一种优雅的方式:InheritableThreadLocal

parent的inheritableThreadLocals不为null时,就会将parentinheritableThreadLocals,赋值给当前线程的inheritableThreadLocals

InheritableThreadLocal实现原理

IheritableThreadLocal直接继承ThreadLocal,只写了三个方法:

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);
 }

在Java线程详解:线程模型,Thread类,异常处理器,异步执行结果Future一文中我们提到过,在创建线程时:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

这个方法会将父线程的inheritableThreadLocals的所有键值对插入到自己的inheritableThreadLocals中。

InheritableThreadLocal与线程池

我们知道线程池的线程复用会带来脏数据问题,但ThreadLocal只需要手动remove即可解决,但InheritableThreadLocal就没这么好办了。

InheritableThreadLocal 无法解决,也就是在使用线程池等会池化复用线程的执行组件情况下,异步执行执行任务,需要传递上下文的情况。

对于线程池中的线程,它们的InheritableThreadLocal,都来自于线程池中第一个执行任务的线程,这就会导致InheritableThreadLocal混乱/失效。

如何在线程池中正确的使用InheritableThreadLocal呢?TransmittableThreadLocal提供了这样的功能。transmittable-thread-local 是阿里巴巴开源的一个工具类,简称TTL,挖个坑,以后有空就来扒一扒TransmittableThreadLocal的源码。

参考文档

讲透 ThreadLocal 和 InheritableThreadLocal

从ThreadLocal谈到TransmittableThreadLocal,从使用到原理

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论