ThreadLocal

再次重复一遍,并发问题出现的根源是多个线程同时读写同一共享变量。通过 Immutable 的方式可以只读不写,还有另一种方式就是:避免共享

比如局部变量是线程安全的,因为每个线程都有自己的一份,没有共享,即线程封闭。Java 提供 ThreadLocal 也能做到避免共享,即每个线程都有自己的副本。

使用方法

我们知道 SimpleDateFormat 不是线程安全的,如果要在并发场景下使用它,有一个办法就是通过 ThreadLocal。

如果是同一个线程调用 get 方法,则会返回相同的实例,若是不同的线程调用 get 方法,则会返回不同的实例。

// java.lang.Thread.java
public class SafeDateFormat {
    private static final ThreadLocal<SimpleDateFormat> th = 
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static DateFormat get() {
        return th.get();
    }
}

原理实现

需要实现 ThreadLocal 的逻辑,我们有两个思路:

  • 在 ThreadLocal 中持有一个 Map,key 是线程 ID,value 是我们需要的值。

  • 在 Thread 中持有一个 Map,key 是 ThreadLocal,value 是我们需要的值。

Java 使用的是第二种思路,理由是:

  • 数据亲缘性:ThreadLocal 只是一个工具类,内部不应该存储与线程相关的数据,与线程相关的数据应该放在 Thread 里面。

  • 不容易产生内存泄漏:ThreadLocal 的生命周期往往比线程长,若数据放在 ThreadLocal中,就算 Thread 已经销毁了,相关的数据也不会被回收;若数据放在 Thread 中,Thread 对象可以被回收,那么 ThreadLocalMap 也可以被回收了。

下面是 Java 1.11 精简后的代码:

注意事项

内存泄漏

上文提到 Java 设计成 Thread 持有 ThreadLocal 数据的原因之一是不容易产生内存泄漏,但是在某些情况下还是会有内存泄漏的问题。

比如在线程池中使用 ThreadLocal。线程池中的线程存活时间很长,往往与程序的生命周期一致,所以 Thread 持有的 ThreadLocalMap 一直不会被回收。尽管 ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用的,只要 ThreadLocal 自己的生命周期结束就可以被回收,但是 Entry 中的 Value 是强引用,所以尽管 Value 的生命周期结束了,也不会被回收,导致内存泄漏。

解决方案:手动释放,比如通过 try-finally。

InheritableThreadLocal

ThreadLocal 创建的线程变量,子线程是无法继承的,也就是说你在线程 A 中创建了 ThreadLocal V,然后又在线程 A 中创建了线程 B,但是在线程 B 中无法访问到 V。

Java 提供了 InheritableThreadLocal 来支持这种需求。

Last updated

Was this helpful?