跳到主要内容

Java 多线程-ThreadLocal 的使用及原理

快速阅览​

ThreadLocal 原理​

ThreadLocal,即线程本地变量。如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。

  • Thread 类有一个类型为 ThreadLocal.ThreadLocalMap 的实例变量 threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
  • ThreadLocalMap 内部维护着 Entry 数组,每个 Entry 代表一个完整的对象,key 是 ThreadLocal 本身,value 是 ThreadLocal 的泛型值。
  • 每个线程在往 ThreadLocal 里设置值的时候,都是往自己的 ThreadLocalMap 里存,读也是以某个 ThreadLocal 作为引用,在自己的 map 里找对应的 key,从而实现了线程隔离。

ThreadLocal 内存泄露问题​

弱引用比较容易被回收。因此,如果 ThreadLocal(ThreadLocalMap 的 Key)被垃圾回收器回收了,但是因为 ThreadLocalMap 生命周期和 Thread 是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap 的 key 没了,value 还在,这就会「造成了内存泄漏问题」。

如何「解决内存泄漏问题」?使用完 ThreadLocal 后,及时调用 remove() 方法释放内存空间。

为什么用弱引用​

1、为什么 ThreadLocalMap 使用弱引用存储 ThreadLocal?

如果在 ThreadLocalMap 的 key 使用强引用,是无法完全避免内存泄漏,ThreadLocal 使用完后,ThreadLocal Reference 被回收,但是 Map 的 Entry 强引用了 ThreadLocal,ThreadLocal 就无法被回收,因为强引用链的存在,Entry 无法被回收,最后会导致 Key 内存泄漏。

因此,使用弱引用可以防止长期存在的线程(通常使用了线程池)导致 ThreadLocal 无法回收造成内存泄漏。

2、那通常说的 ThreadLocal 内存泄漏是如何引起的呢?

我们注意到 Entry 对象中,虽然 Key(ThreadLocal) 是通过弱引用引入的,但是 value 即变量值本身是通过强引用引入。

由于 ThreadLocalMap 和线程的生命周期是一致的,当线程资源长期不释放,即使 ThreadLocal 本身由于弱引用机制已经回收掉了,但 value 还是驻留在线程的 ThreadLocalMap 的 Entry 中。即存在 key 为 null,但 value 却有值的无效 Entry。导致内存泄漏。

ThreadLocal 的应用场景​

  • 数据库连接池
  • 会话管理中使用

ThreadLocal 线程局部变量​

ThreadLocal 类可以给每个线程维护一个独立的变量副本,使多线程的场景使用共有的 ThreadLocal 变量,同时每个线程在 ThreadLocal 对象中保存的变量副本是相互隔离的。

ThreadLocal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对 ThreadLocal< String > 而言即为 String 类型变量),在不同的 Thread 中有不同的副本。这里有几点需要注意

  • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来
  • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题
  • 既无共享,何来同步问题,又何来解决同步问题一说?

那 ThreadLocal 到底解决了什么问题,又适用于什么样的场景?

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被 private static 修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

使用场景​

总的来说,ThreadLocal 适用于 每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。另外,该场景下,并非必须使用 ThreadLocal ,其它方式完全可以实现同样的效果,只是 ThreadLocal 使得实现更简洁。

如果开发者希望将类的某个静态变量(user ID或者transaction ID)与线程状态关联,则可以考虑使用 ThreadLocal。

如上所述,ThreadLocal 适用于如下两种场景

  • 每个线程需要有自己单独的实例
  • 实例需要在多个方法中共享,但不希望被多线程共享

对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLocal 可以以非常方便的形式满足该需求。

对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现(享元模式)。ThreadLocal 使得代码耦合度更低,且实现更优雅。

使用方式​

假设,有这样一个类:

@Data
@AllArgsConstructor
public class Counter{
private int count;
}

我们希望多线程访问 Counter 对象时,每个线程各自保留一份 count 计数,那可以这么写:

ThreadLocal<Counter> threadLocal = new ThreadLocal<>();
threadLocal.set(new Counter(0));
Counter counter = threadLocal.get();

如果我们不想每次调用的时候都去初始化,则可以重写 ThreadLocal 的 initValue() 方法给 ThreadLocal 设置一个对象的初始值:

ThreadLocal<Counter> threadLocal = new ThreadLocal<Counter>() {
@Override
protected Counter initialValue() {
return new Counter(0);
}
};

如下所示,这些线程执行的 myTask 对象是同一个,按理说这个 threadLocal 变量会在执行时被其它的线程修改,但是实际输出发现它们都是与当前线程名一致的,说明两个线程在 set 值的时候值是隔离的。进而说明每个线程都创建了一个 threadLocal 变量的副本

public class Temp {
public static void main(String[] args) {
MyTask myTask = new MyTask();
new Thread(myTask, "线程A").start();
new Thread(myTask, "线程B").start();
new Thread(myTask, "线程C").start();
new Thread(myTask, "线程D").start();
new Thread(myTask, "线程E").start();
}

static class MyTask implements Runnable {
// 注意:这里创建的是 static 方法,按理来说整个 JVM 生命周期只有一份,但是使用了这个 ThreadLocal 后每个线程的都是不同的
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

@Override
public void run() {
threadLocal.set("当前这个变量值是" + Thread.currentThread().getName());
System.out.println(
Thread.currentThread().getName() + ":" + threadLocal.get() +
" threadLocal的HashCode:" + threadLocal.hashCode() +
" threadLocal内的值的HashCode:" + threadLocal.get().hashCode());

threadLocal.remove(); // 用完要释放掉
}
}
}

输出为:可以发现 threadLocal 的 HashCode都是一样的,只有它们内部的值不一样

线程A:当前这个变量值是线程A   threadLocal的HashCode:493184190   threadLocal内的值的HashCode:1708679936
线程C:当前这个变量值是线程C threadLocal的HashCode:493184190 threadLocal内的值的HashCode:1708679938
线程E:当前这个变量值是线程E threadLocal的HashCode:493184190 threadLocal内的值的HashCode:1708679940
线程D:当前这个变量值是线程D threadLocal的HashCode:493184190 threadLocal内的值的HashCode:1708679939
线程B:当前这个变量值是线程B threadLocal的HashCode:493184190 threadLocal内的值的HashCode:1708679937

实现原理​

ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类,Map 的 Key 是 ThreadLocal 实例本身,value 是要存储的值。

每个线程内部都有一个 ThreadLocalMap(注意!!!! 这个 Map 是存在线程里面的,所以生命周期和线程一致!!!),Map 里面存放的是 ThreadLocal 对象和线程的变量副本。Thread 内部的 Map 通过 ThreadLocal 对象来维护,向 map 获取和设置变量副本的值。

结构如图:

不同的线程,每次获取变量值时,只能获取自己对象的副本的值。实现了线程之间的数据隔离。

先来看这个 get、set 方法,可以发现它们都是操作线程里面的 ThreadLocalMap

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

JDK1.8 的设计相比于之前的设计(通过 ThreadMap 维护了多个线程和线程变量的对应关系,key 是 Thread 对象,value 是线程变量)的好处在于,每个 Map 存储的 Entry 数量变少了,线程越多键值对越多。

现在的键值对的数量是由 ThreadLocal 的数量决定的,一般情况下 ThreadLocal 的数量少于线程的数量,而且并不是每个线程都需要创建 ThreadLocal 变量。当 Thread 销毁时,ThreadLocalMap 也会随之销毁,减少了内存的使用,之前的方案中线程销毁后,ThreadLocalMap 仍然存在。

public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
// 这里销毁的是 entry for key.
m.remove(this);
}
}

ThreadLocal源码解析​

set 方法​

首先获取线程,然后获取线程的 Map。

如果 Map 不为空则将当前 ThreadLocal 的引用作为 key 设置到 Map 中(注意!!!这个 Map 来自 Thread)。如果 Map 为空,则创建一个 Map 并设置初始值。

如上述代码所示,我们可以看出来每个线程持有一个 ThreadLocalMap 对象。每创建一个新的线程 Thread 都会实例化一个 ThreadLocalMap 并赋值给成员变量 threadLocals,使用时若已经存在 threadLocals 则直接使用已经存在的对象;否则的话,新创建一个 ThreadLocalMap 并赋值给 threadLocals 变量。

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

如上述代码所示,其为 Thread 类中关于 threadLocals 变量的声明。

get 方法​

首先获取当前线程,然后获取 Map。

如果 Map 不为空,则 Map 根据 ThreadLocal 的引用来获取 Entry,如果 Entry 不为空,则获取到 value 值返回。如果 Map 为空或者 Entry 为空,则初始化并获取初始值 value,然后用 ThreadLocal 引用和 value 作为 key 和 value 创建一个新的 Map。

使用这个 ThreadLocal 当 key 是因为线程内部存在一个 Map,而运行时可以使用多个 ThreadLocal 所以需要通过 ThreadLocal 指针来当这个 Map 的 key 来取得对应的 ThreadLocal

这个 Entry 使用弱引用:

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

所以当时的疑惑,如果要取得的 Entry 被回收了(弱引用),这时调用 get 怎么办,实际上就是通过这个 setInitialValue 方法来保证能取得值的(取得的值为 null 则再次初始化一个值出来 ~)

remove 方法​

删除当前线程中保存的 ThreadLocal 对应的实体 Entry

public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
// 这里销毁的是 entry for key.
m.remove(this);
}
}

在学习 Servlet 时了解到,在 doGet() 或 doPost() 方法中,如果使用了 ThreadLocal,但没有清理,那么它的状态很可能会影响到下次的某个请求,因为 Servlet 容器很可能用线程池实现线程复用。

initialValue 方法​

该方法的第一次调用发生在当线程通过 get 方法访问线程的 ThreadLocal 值时。除非线程先调用了 set 方法,在这种情况下, initialValue 才不会被这个线程调用。

该方法只返回一个 null,如果想要线程变量有初始值需要通过子类继承 ThreadLocal 的方式去重写此方法,通常可以通过匿名内部类的方式实现。这个方法是 protected 修饰的,是为了让子类覆盖而设计的。

例如如果需要实例化的对象可以使用 ThreadLocal.withInitial 静态方法,例如下面创建一个 SimpleDateFormat 对象

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

通常,每个线程最多调用一次此方法,但是在调用 remove 后再调用 get,可以再次调用此方法。

ThreadLocalMap​

ThreadLocalMap 是 ThreadLocal 的静态内部类,没有实现 Map 接口,独立实现了 Map 的功能,内部的 Entry 也是独立实现的。

与 HashMap 类似,初始容量默认是16,初始容量必须是2的整数幂。通过 Entry 类的数据 table 存放数据。size 是存放的数量,threshold是扩容阈值。

因此 ThreadLocal 其实只是个符号意义,本身不存储变量,仅仅是用来索引各个线程中的变量副本。

Entry 继承自 WeakReference,key 是弱引用,其目的是将 ThreadLocal 对象的生命周期和线程生命周期解绑。

弱引用是啥?​

Java 语言中为对象的引用分为了四个级别,分别为 强引用 、软引用、弱引用、虚引用。弱引用具体指的是 java.lang.ref.WeakReference 类。

对对象进行弱引用不会影响垃圾回收器回收该对象,即如果一个对象只有弱引用存在了,则下次 GC 将会回收掉该对象(不管当前内存空间足够与否)。

再来说说内存泄漏,假如一个短生命周期的对象被一个长生命周期对象长期持有引用,将会导致该短生命周期对象使用完之后得不到释放,从而导致内存泄漏。

因此,弱引用的作用就体现出来了,可以使用弱引用来引用短生命周期对象,这样不会对垃圾回收器回收它造成影响,从而防止内存泄漏。

弱引用和内存泄漏是啥?​

内存溢出:没有足够的内存供申请者提供

内存泄漏:程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等验证后沟。内存泄漏的堆积会导致内存溢出。

弱引用:垃圾回收器一旦发现了弱引用的对象,不管内存是否足够,都会回收它的内存。

内存泄漏的根源是 ThreadLocalMap 和 Thread 的生命周期是一样长的。

Entry 弱引用的作用​

1、为什么 ThreadLocalMap 使用弱引用存储 ThreadLocal?

如果在 ThreadLocalMap 的 key 使用强引用还是无法完全避免内存泄漏,ThreadLocal 使用完后,ThreadLocal Reference 被回收,但是 Map 的 Entry 强引用了 ThreadLocal,ThreadLocal 就无法被回收,因为强引用链的存在,Entry 无法被回收,最后会内存泄漏。

因此,使用弱引用可以防止长期存在的线程(通常使用了线程池)导致 ThreadLocal 无法回收造成内存泄漏。

2、那通常说的 ThreadLocal 内存泄漏是如何引起的呢?

我们注意到 Entry 对象中,虽然 Key(ThreadLocal) 是通过弱引用引入的,但是 value 即变量值本身是通过强引用引入。

由于 ThreadLocalMap 和线程的生命周期是一致的,当线程资源长期不释放,即使 ThreadLocal 本身由于弱引用机制已经回收掉了,但 value 还是驻留在线程的 ThreadLocalMap 的 Entry 中。即存在 key 为 null,但 value 却有值的无效 Entry。导致内存泄漏。

但实际上,ThreadLocal 内部已经为我们做了一定的防止内存泄漏的工作。

即如下方法:

/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;

for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

上述方法的作用是擦除某个下标的 Entry(置为 null,可以回收),同时检测整个 Entry[] 表中对 key 为 null 的 Entry 一并擦除,重新调整索引。

该方法,在每次调用 ThreadLocal 的 get、set、remove 方法时都会执行,即 ThreadLocal 内部已经帮我们做了对 key 为 null 的 Entry 的清理工作。

但是该工作是有触发条件的,需要调用相应方法,假如我们使用完之后不做任何处理是不会触发的。

所以在代码逻辑中使用完 ThreadLocal,都要调用remove方法,及时清理。

目前我们使用多线程都是通过线程池管理的,对于核心线程数之内的线程都是长期驻留池内的。显式调用 remove,一方面是防止内存泄漏,最为重要的是,不及时清除有可能导致严重的业务逻辑问题,产生线上故障(使用了上次未清除的值)。

最佳实践:在 ThreadLocal 使用前后都调用 remove 清理,同时对异常情况也要在 finally 中清理。

ThreadLocalMap 的构造方法​

如下所示:

构造函数创建一个长队为16的 Entry 数组,然后计算 firstKey 的索引,存储到 table 中,设置 size 和 threshold。

firstKey.threadLocalHashCode & (INITIAL_CAPACITY-1) 用来计算索引,nextHashCode 是 Atomicinteger 类型的,Atomicinteger 类是提供原子操作的 Integer 类,通过线程安全的方式来加减,适合高并发使用。

每次在当前值上加上一个 HASH_INCREMENT 值,这个值和斐波拉契数列有关,主要目的是为了让哈希码可以均匀的分布在2的n次方的数组里,从而尽量的避免冲突。

当 size为 2的幂次的时候,hashCode & (size - 1) 相当于取模运算 hashCode % size,位运算比取模更高效一些。为了使用这种取模运算, 所有 size 必须是 2的幂次。这样一来,在保证索引不越界的情况下,减少冲突的次数。

ThreadLocalMap的 set 方法​

Map 的 key 是啥?​

首先看下这个 get 方法,可以发现它内部调用的 map.getEntry(this) 说明 key 是以当前 ThreadLocal 实例对象

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

从 ThreadLocalMap 中取数据,有数据就返回,没有数据就设置默认值并返回(所以说 initialValue 是延迟调用)。

value 为什么是强引用​

不设置为弱引用,是因为不清楚这个 Value 除了 map 的引用还是否还存在其他引用,如果不存在其他引用,当 GC 的时候就会直接将这个 Value 干掉了,而此时我们的 ThreadLocal 还处于使用期间,就会造成 Value 为 null 的错误,所以将其设置为强引用。

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

而为了解决这个强引用得不到释放的问题(就是等太久 Key 已经被释放了,强引用还没释放),它提供了一种机制就是上面说的将 Key 为 Null 的 Entity 直接清除

Hash 冲突的解决​

ThreadLocalMao 使用了线性探测法来解决冲突。线性探测法探测下一个地址,找到空的地址则插入,若整个空间都没有空余地址,则产生溢出。例如:长度为8的数组中,当前key的hash值是6,6的位置已经被占用了,则hash值加一,寻找7的位置,7的位置也被占用了,回到0的位置。直到可以插入为止,可以将这个数组看成一个环形数组。

Reference​

参考资料 详述 ThreadLocal 的实现原理及其使用方法 参考资料 Java多线程之深入解析ThreadLocal和ThreadLocalMap 参考资料 ThreadLocal为啥要用弱引用?不知道 参考资料 Java进阶(七)正确理解Thread Local的原理与适用场景 参考资料 ThreadLocal弱引用与内存泄漏分析