并发编程 ThreadLocal

本文将介绍并发编程中的 ThreadLocal 及线程存储模式。

一、什么是线程存储模式?

如果把共享数据的可见范围限制在线程以内,就可以避免并发问题,线程存储模式就是这样一个模式。

二、线程存储方法

1. 简单做法

在 ThreadLocal 的内部持有一个 Map,它的 key 是线程,value 是变量。保存与获取变量时,根据线程找到对应的 value。

2. ThreadLocal 的做法

将线程变量缓存在线程中。Thread 对象中持有一个 ThreadLocalMap,其 key 为 ThreadLocal,其值为具体的线程变量值。

实际拿取时,ThreadLocal 会先获取到代表当前线程的 Thread 对象,然后以自身作为 key 获取 value。具体的拿取细节都已经被 ThreadLocal 隐藏,实际使用时我们只需要实例化 ThreadLocal、调用其 get()set() 方法即可。

下图是 ThreadLocal 的 get() 方法源码:

3. ThreadLocal 的做法的好处

  • 线程的数据本身就属于线程,因此将数据放置到 Thread 对象中更符合逻辑

  • 在 “简单做法” 中,ThreadLocal 内部的 Map 持有 Thread 对象的引用,ThreadLocal 的存活时间往往较长,这将影响 Thread 对象的回收

    线程 A 已经执行结束,理应回收,但由于 ThreadLocal 持有所有线程的引用,因此得等待 ThreadLocal 回收后线程 A 才有可能回收

三、ThreadLocal

ThreadLocal 用于设置线程变量。

注意:ThreadLocal 用于设置而非存储线程变量,线程变量存储在 Thread 中

线程变量在每个线程中都有单独的副本,使用线程变量可以解决并发访问问题。

四、ThreadLocal 的简单使用

在并发场景中,利用 ThreadLocal 使用线程不安全的 SimpleDateFormat 的示例:

1
2
3
4
5
6
7
8
9
public class Test {

static final ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

public static void main(String[] args) {
SimpleDateFormat simpleDateFormat = threadLocal.get();
}

}

五、ThreadLocal 的存储结构

  • Thread 对象中持有一个 ThreadLocalMap
  • ThreadLocalMap 中维护了一个 Entry 数组,它是一个哈希表
  • Entry 即是 Thread 对线程变量键值对的存储,它继承了 WeakReference,key 是对 threadLocal 的弱引用,value 是线程变量

六、内存泄露问题

1. 说明

使用 ThreadLocal 可能导致内存泄漏。

原因在于:

  • 线程往往存活时间很长
  • 线程对象持有的 ThreadLocalMap 将和线程同生共死
  • ThreadLocalMap 持有的 Entry 将与 ThreadLocalMap 同生共死
  • Entry 对 key 是弱引用,当 key 不再被其它其它地方引用时,key 可以被回收
  • Entry 对 value 是强引用,value 将与 Entry 同生共死

因此,线程变量值往往会一直存在且无法被回收。

2. 解决办法

通过手动释放解决。

1
2
3
4
5
6
7
8
9
10
11
12
ExecutorService es;
ThreadLocal tl;
es.execute(() -> {
//ThreadLocal增加变量
tl.set(obj);
try {
// 省略业务逻辑代码
} finally {
//手动清理ThreadLocal
tl.remove();
}
});

七、ThreadLocal 的继承性

如果在线程中创建子线程,子线程无法继承父线程的线程变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {

static final ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();

public static void main(String[] args) {
threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
System.out.println(threadLocal.get());
new Thread(() -> {
System.out.println(threadLocal.get());
}).start();
}

}

JUC 提供了 InheritableThreadLocal,它允许子线程继承父线程的线程变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test1 {

static final InheritableThreadLocal<SimpleDateFormat> threadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {
threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
System.out.println(threadLocal.get());
new Thread(() -> {
System.out.println(threadLocal.get());
}).start();
}

}

参考

  • Java 并发编程实战