Skip to main content

ThreadLocal

David LiuAbout 6 min

ThreadLocal

解决线程安全问题的另一种思路,之前是共享资源加锁或 CAS 重试,现在是线程隔离各用各的。

作用:

  • 实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题
  • 实现了线程内的资源共享

局部变量:可以线程隔离,但是不能跨方法。Thread Local 主要解决的就是这个跨方法的问题

线程关联的原理

ThreadLocal 并不是一个独立的存在, 它与 Thread 类是存在耦合的, java.lang.Thread 类针对 ThreadLocal 提供了如下支持:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class.
 */
ThreadLocal.ThreadLocalMap threadLocals = null;

Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap

ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocalvalue为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。

每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离

ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。

我们还要注意Entry, 它的keyThreadLocal<?> k ,继承自WeakReference, 也就是我们常说的弱引用类型。

为什么 Map 的 key 要设置成弱引用呢?

因为如果我们 ThreadLocalMap 中的 ThreadLocal 不设置成弱引用,设置成强引用的话,如果外界已经将所有引用 ThreadLocal 的地方设置为了 null(也就是不再使用了),但是我们的 Map 里的 key 还指向堆内存里的 ThreadLocal 呢,而我们又不能直接操控 Map。

并且这个线程始终在运行(比如说线程池复用连接),那么久而久之,堆内存里的 ThreadLocal 就无法被回收,造成内存泄露

而设计成弱引用的话,在每次 GC 时,发现没有其他强引用指向 ThreadLocal 了,便会将其回收。

概括说就是:在方法中新建一个线程本地对象,就有一个强引用指向它,在调用 set()后,线程的 ThreadLocaMap 对象里的 Entry 对象又有一个引用 k 指向它。如果后面这个引用 k 是强引用就会使方法执行完,栈帧中的强引用销毁了,对象还不能回收,造成严重的内存泄露。

那为什么不设置成软引用呢?

让没有引用的尽快被回收,而不用等到内存不够再回收

内存泄漏

ThreadLocalThreadLocalMap 中是以一个弱引用身份被 Entry 中的 Key 引用的,因此如果 ThreadLocal 没有外部强引用来引用它,那么 ThreadLocal 会在下次 JVM 垃圾收集时被回收。这个时候就会出现 Entry 中 Key 已经被回收,出现一个 null Key 的情况,外部读取 ThreadLocalMap 中的元素是无法通过 null Key 来找到 Value 的。因此如果当前线程的生命周期很长,一直存在,那么其内部的 ThreadLocalMap 对象也一直生存下来,这些 null key 就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致 Entry 不会回收,Value 也不会回收,但 Entry 中的 Key 却已经被回收的情况,造成内存泄漏。

但是 JVM 团队已经考虑到这样的情况,并做了一些措施来保证 ThreadLocal 尽量不会内存泄漏:在 ThreadLocal 的 get()、set()、remove()方法调用的时候会清除掉线程 ThreadLocalMap 中所有 Entry 中 Key 为 null 的 Value,并将整个 Entry 设置为 null,利于下次内存

由于ThreadLocalMap的 key 是弱引用,而 Value 是强引用。这就导致了一个问题,ThreadLocal 在没有外部对象强引用时,发生 GC 时弱引用 Key 会被回收,而 Value 不会回收,如果创建 ThreadLocal 的线程一直持续运行,那么这个 Entry 对象中的 value 就有可能一直得不到回收,发生内存泄露。

线程池脏读问题

ThreadLocal 是利用独占资源的方式,来解决线程安全问题,那如果我们确实需要有资源在线程之间共享,应该怎么办呢?这时,我们可能就需要用到线程安全的容器了。

上个例子说明,ThreadLocal 用不好也会产生副作用,线程复用会产生脏数据。由于线程 池会重用 Thread 对象,那么与 Thread 绑定的类的静态属性 ThreadLocal 变量也会被重用。如果在实现的线程 run()方法体中不显式地调用于线程相关的 ThreadLocal 信息,那么倘若下一个线程不调用 set()设置初始值,就可能 get 到重用的线程信息,包括 ThreadLocal 所关联的线程对象的 value 值。

解决方案也很简单,在每个线程执行中,往 ThreadLocal 对象设置值后,执行完核心逻辑代码,最后对 ThreadLocal 对象进行清理。优化后的代码如下:

父子线程间共享

我们使用ThreadLocal的时候,在异步场景下是无法给子线程共享父线程中创建的线程副本数据的。

为了解决这个问题,JDK 中还有一个InheritableThreadLocal类,我们来看一个例子:

public class InheritableThreadLocalDemo {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
        ThreadLocal.set("父类数据:threadLocal");
        inheritableThreadLocal.set("父类数据:inheritableThreadLocal");

        new Thread(() -> {
            System.out.println("子线程获取父类ThreadLocal数据:" + threadLocal.get());
            System.out.println("子线程获取父类inheritableThreadLocal数据:" + inheritableThreadLocal.get());
        }).start();
    }
}

打印结果:

  • 子线程获取父类 ThreadLocal 数据:null
  • 子线程获取父类 inheritableThreadLocal 数据:父类数据:inheritableThreadLocal

实现原理是子线程是通过在父线程中通过调用new Thread()方法来创建子线程,Thread#init方法在Thread的构造方法中被调用。在init方法中拷贝父线程数据到子线程中:

InheritableThreadLocal仍然有缺陷,一般我们做异步化处理都是使用的线程池,而InheritableThreadLocal是在new Thread中的init()方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。

解决问题方案:阿里巴巴的TransmittableThreadLocal组件就可以解决这个问题。

MDC

MDC(Mapped Diagnostic Context,映射调试上下文)是 Slf4J, log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能,也可以说是一种轻量级的日志跟踪工具。

canal