ThreadLocal源码深度解析


想必很多朋友对ThreadLocal并不陌生,今天我们就来一起探讨下ThreadLocal实现原理和相关源码。首先,本文先介绍一下ThreadLocal以及底层结构,然后从ThreadLocal相关的API入手,详细分析ThreadLocal源码和使用时需要注意的地方,最后分析下ThreadLocal中的内存泄露问题。

1. ThreadLocal概述

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。我们首先来看下JDK中的解释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g.a user ID or Transaction ID).
Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist)

也就是说这个类给线程提供了一个本地变量,这个变量是该线程自己拥有的。在该线程存活和ThreadLocal实例能访问的时候,保存了对这个变量副本的引用。当线程消失的时候,所有的线程本地实例都会交由GC接管。

1.1. 举个栗子

我们先来看一个简单的demo,了解下如何在代码中使用ThreadLocal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class ThreadLocalDemo {
// ThreadLocal instances are typically private static fields in classes
private static ThreadLocal<List<String>> threadLocal = new ThreadLocal<>();

public void setThreadLocal(List<String> values) {
threadLocal.set(values);
}

public void getThreadLocal() {
System.out.println(Thread.currentThread().getName());
threadLocal.get().forEach(name -> System.out.println(name));
}

public static void main(String[] args) throws InterruptedException {

final ThreadLocalDemo threadLocal = new ThreadLocalDemo();
new Thread(() -> {
List<String> params = new ArrayList<>(3);
params.add("张三");
params.add("李四");
params.add("王五");
threadLocal.setThreadLocal(params);
threadLocal.getThreadLocal();
}).start();

new Thread(() -> {
try {
Thread.sleep(1000);
List<String> params = new ArrayList<>(2);
params.add("Chinese");
params.add("English");
threadLocal.setThreadLocal(params);
threadLocal.getThreadLocal();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}

运行结果如下:

1
2
3
4
5
6
7
Thread-0
张三
李四
王五
Thread-1
Chinese
English

可以,看出虽然多个线程对同一个变量进行访问,但是由于threadLocal变量由ThreadLocal 修饰,则不同的线程访问的就是该线程设置的值,这里也就体现出来ThreadLocal的作用。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

2. ThreadLocal 的实现

假设我们要设计一个和线程绑定的变量,我们会怎么做呢?
很常见的一个思路就是把Thread和变量值放在一个Map中,即key为对应的Thread,value为变量值。
那么JDK是不是按这种方式实现的呢?答案:显然不是。我们这里先不讨论JDK为什么不用这种方案,后文会详细介绍。

2.1. ThreadLocal内部结构

要理解ThreadLocal的内部结构,需要弄清楚以下三个关键的类:

  • Thread
  • ThreadLocal
  • ThreadLocalMap

他们之间的关系如下图:
QHAArF.png

java.lang.Thread 内有个 ThreadLocalMap :

1
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap 存储了所有跟该 Thread 绑定的 ThreadLocal 对象到该 ThreadLocal 对象对应 Value 的映射。每次在读取一个 ThreadLocal 对象的值的时候,就是通过 ThreadLocalMap 来查看 ThreadLocal 对象对应的值是什么。因为每个 Thread 都有自己的 ThreadLocalMap,所以同一个 ThreadLocal 对象在不同的 Thread 里能对应不同的 Value。从而实现 ThreadLocal 的功能。

ThreadLocalMap,从字面意义上来看,它是一个Map,大家可以类比下Java中常见的HashMap,在基本原理以及使用方式上,两者比较类似。

ThreadLocalMap在实现方式上,是通过数组实现 Map 功能的。数组的元素是 Entry,是个装有 Key Value 的对象。Key 是个弱引用,所以上图用虚线表示,指向一个 ThreadLocal 对象,Value 是强引用指向跟该 ThreadLocal 对象绑定的 Value 值。

再看一张网络上的图片,应该可以更好的理解,如下图:
QHA8qe.png

2.2. ThreadLocal API

下面将详细分析ThreadLocal的源码,我们从ThreadLocal提供的API入手。

2.2.1. ThreadLocal 的 set 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
// 1. 获得当前线程
Thread t = Thread.currentThread();
// 2. 拿到当前线程对应的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 3. 判断map是否为null
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

set方法,将此线程局部变量的当前线程副本中的值设置为指定值。

我们来详细看源码。首先,获得当前线程,然后根据当前线程去获取对应的ThreadLocalMap。那我们继续看 getMap(t) 又做了什么。

1
2
3
4
5
6
7
8
9
10
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

很简单,就是返回当前线程的threadLocals变量,前面提到过,这个变量就是当前线程中的ThreadLocalMap。

我们接着set方法往下看。首先会判断map是否为空,如果不为空,那么调用ThreadLocalMap的set方法,设置值;如果为空,则创建一个ThreadLocalMap。

假设我们是第一次调用set方法,那么map肯定为空,所以会调用createMap方法,我们接着看createMap方法又做了什么:

1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

可以看到,createMap方法创建了一个新的ThreadLocalMap并赋初值,将我们的传入的值保存到ThreadLocalMap中。ThreadLocalMap的构造函数,我们将在下文详细介绍。第一次调用set方法的过程到这里基本上就算是结束了。

我们接着回到set代码:

1
2
if (map != null)
map.set(this, value);

可以看到,如果map不为空,会调用map的set方法,将key(也就是当前ThreadLocal对象)、value(set方法传入的值)保存到ThreadLocalMap中。有关于ThreadLocalMap的set方法,也将在下文中介绍。

到此,ThreadLocal的set方法,全部分析完毕。这里做个总结:

  1. 获取当前线程;
  2. 拿到当前线程的ThreadLocalMap,如果不为空,转3;否则转4;
  3. map不为空,调用ThreadLocalMap的set方法,将当前ThreadLocal对象和传入的value值,保存到map中,方法结束。
  4. map为空,创建一个ThreadLocalMap,赋值给当前线程的threadLocals变量(也就是当前线程绑定的ThreadLocalMap),然后将当前ThreadLocal对象和传入的value值,保存到map中,方法结束。

2.2.2. ThreadLocal的get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
// 1. 获取当前线程
Thread t = Thread.currentThread();
// 2. 拿到当前线程对应的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 3. 判断map是否为null
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

get方法,返回此线程局部变量的当前线程副本中的值。如果这是线程第一次调用该方法,则创建并初始化此副本。

我们直接看源码。可以看到,get方法的前3步骤与set方法相同,不再赘述。
我们这里看关键的第三步,判断map是否为null。我们知道,初始情况下,Thread对象中的map变量为null:

1
ThreadLocal.ThreadLocalMap threadLocals = null;

而在上面的set方法分析中可以知道,只要调用过set方法后,map必然不为空。所以调用get方法时,如果事先已经调用过set方法赋值,那么这里map肯定不为null。而如果没有事先调用set,那么直接调用get方法,则map为null。

我们首先来看map不为null的情况:

1
2
3
4
5
6
7
8
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}

可以看到,跟set方法类似,也是交给ThreadLocalMap去处理:调用map的getEntry方法(ThreadLocalMap的处理方法,我们统一在后文中介绍)。拿到对应的值后返回,get方法结束。

如果map为null,我们来看:

1
return setInitialValue();

返回的是 setInitialValue 方法的值。那么我们接着看这个setInitialValue方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

这个方法是不是和set方法十分类似?实际上,作者也在注释里写得很清楚了:

Variant of set() to establish initialValue

这个方法就是set方法的一个变种,唯一的区别是:set方法中的value是传入的,而这个方法的value是调用 initialValue 方法获得的。没错,我们接下来就是要去看 initialValue 方法干了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Returns the current thread's "initial value" for this
* thread-local variable. This method will be invoked the first
* time a thread accesses the variable with the {@link #get}
* method, unless the thread previously invoked the {@link #set}
* method, in which case the {@code initialValue} method will not
* be invoked for the thread. Normally, this method is invoked at
* most once per thread, but it may be invoked again in case of
* subsequent invocations of {@link #remove} followed by {@link #get}.
*
* <p>This implementation simply returns {@code null}; if the
* programmer desires thread-local variables to have an initial
* value other than {@code null}, {@code ThreadLocal} must be
* subclassed, and this method overridden. Typically, an
* anonymous inner class will be used.
*
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null;
}

这个方法比较特殊:

  • 直接返回null
  • 方法声明为protected
  • 注释比方法本身更有内容

作者的注释写了一大堆,其实好好看注释就能明白这个方法的作用了。简单总结下:

这个方法用于在第一次调用get方法而事先没有调用set方法时返回初始值,默认情况下,该初始值为null。如果用户不想得到默认的null,那么应该用ThreadLocal的子类并重写该方法,通常使用一个匿名内部类来实现。

所以也就不难理解为什么这个方法是protected,通常来说,在Java中,protected声明的方法,都是建议子类来重写的方法。

假设使用ThreadLocal时,没有用子类来重写initialValue方法,也没有在调用get之前先调用set方法,那么get方法默认得到的就是null。这一点需要注意,如果不清楚这一点,很容易就会产生NPE。

get方法到这里也就结束了,总结下:

  1. 获取当前线程;
  2. 拿到当前线程的ThreadLocalMap,如果不为空,转3;否则转4;
  3. map不为空,调用ThreadLocalMap的getEntry方法,将之前通过set方法保存进去的Entry拿出来,返回对应的值,方法结束。
  4. map为空,调用setInitialValue,该方法类似于set方法,只不过value值是初始值,初始值要么为null,要么是子类重写的初始值。返回初始值,方法结束。

2.2.3. ThreadLocal的remove方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

remove方法,移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialValue。

我们直接看源码。同样,也是先获得与当前线程绑定的ThreadLocalMap,如果map不为空,将删除操作委托给map处理。

remove方法的作用很关键,它能够有效避免内存泄露,这个话题我们将在后文中讨论到。

3. 核心的ThreadLocalMap

从上文的讨论中,我们发现,不管是ThreadLocal的底层实现结构,还是ThreadLocal的API中,最核心的就是这个ThreadLocalMap

下面我们就来好好分析下这个ThreadLocalMap。

3.1. Hash冲突处理

在详细讲解ThreadLocalMap之前,我们先要了解ThreadLocalMap的Hash冲突处理,因为这是整个ThreadLocalMap最核心的地方,理解了这个,ThreadLocalMap其他的内容也就比较好理解了。

首先我们回顾下Java中的HashMap,我们知道HashMap的实现方式是* 数组 + 链表*,其中数组用于Hash桶定位,链表用于解决Hash冲突。

ThreadLocalMap,本质上来讲,也是一个Map,也用到了Hash算法,那么它在实现上与HashMap有什么区别呢?这里先把结论给出来:

Hash冲突的处理方式不一样,HashMap使用 链地址法 来解决Hash冲突,而ThreadLocalMap使用 开放地址法 来解决Hash冲突。

每一个 ThreadLocal 对象都有一个 threadLocalHashCode,在将 ThreadLocal 对象及其对应 Value 放入 ThreadLocalMap 时,先根据 threadLocalHashCode 和 ThreadLocalMap 内数组大小用类似于 threadLocalHashCode % ThreadLocalMap.length() 的方法计算出来该 threadLocalHashCode 对应的哪一个 Slot 的 index,再构造 Entry 放入该 index 指向的 ThreadLocalMap 的 Slot 中。

因为 ThreadLocalMap 内数组大小有限,类似于 threadLocalHashCode % ThreadLocalMap.length() 计算 index 的方法可能出现两个不同的 ThreadLocal 对象带着两个不同的 threadLocalHashCode 但被 Hash 到同一个 Slot 的情况(即发生Hash冲突),如下图 ThreadLocalA ThreadLocalB ThreadLocalC 都具有相同的 threadLocalHashCode,在插入 ThreadLocalC 时,根据其 threadLocalHashCode 先被 Hash 到 ThreadLocalA 的 Slot,发现 Slot 不为空,于是 index + 1 再判断临近的已经存了 ThreadLocalB 的 Slot 是否为空,不为空则继续 index + 1 直到找到一个空的 Slot 将 ThreadLocalC 存入。

QHAtIA.png

也就是说,ThreadLocalMap发生Hash冲突时,会顺着当前Hash桶往下找(index + 1),直到找到一个为空的桶,然后将Entry放入。

这种碰撞处理方式也就导致:

每个 ThreadLocal 对象的 threadLocalHashCode 不能挨得太近,不然冲突会很多。

其计算方法如下:

1
2
private static AtomicInteger nextHashCode = new AtomicInteger();
private final int threadLocalHashCode = nextHashCode.getAndAdd(0x61c88647)

0x61c88647 是个很神奇的数字,据说是来自斐波那契散列,用这个数字,产生Hash冲突的概率较低。

ThreadLocalMap 一定不能完全装满,内置的数组一定要比实际存入的 ThreadLocal 对象至少大 1。事实上 ThreadLocalMap 的 Load Factor 是 2/3,超过之后会 rehash 并扩容。不能完全装满的原因是比如还是上面那张图,要获取一个 ThreadLocal 对象对应的 Value,并且这个目标 ThreadLocal 没有存入该 ThreadLocalMap,它的 threadLocalHashCode 刚好也 Hash 到了 ThreadLocalA 对象所在的 Slot,获取的时候先判断目标 ThreadLocal 是不是等于 ThreadLocalA,不是的话再判断是不是等于 ThreadLocalB,依次类推直到获取到一个空 Slot 从而才能知道该 ThreadLocal 没有存储在当前 ThreadLocalMap 中。如果 ThreadLocalMap 完全装满,就不能依赖这个 Slot 是否为空的判断了;

3.2. ThreadLocalMap API

讲完了ThreadLocalMap最核心的Hash冲突处理后,我们来看看相关的API。

从上文分析ThreadLocal的API过程中,我们发现,与ThreadLocal相关的的API有:

  • ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
  • set(ThreadLocal<?> key, Object value)
  • getEntry(ThreadLocal<?> key)
  • remove(ThreadLocal<?> key)

下面将详细介绍这些API以及相关的源码。

3.2.1. ThreadLocalMap构造函数

在前面分析ThreadLocal的set方法中,我们知道,如果当前Thread对应的ThreadLocalMap为null,则会调用createMap方法创建ThreadLocalMap:

1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

即调用了ThreadLocalMap的构造函数,我们来看看构造函数源码:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

同样,注释里面说的很明白:创建一个新的map,并将firstKey、firstValue存入map。

我们看这里的代码,是不是跟HashMap很类似?包括Entry数组table、Hash桶定位过程中的按位与。
这里我们需要特别的关注下Entry对象。

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

我们可以看到,Entry类继承了WeakReference,即弱引用。本文一开始就提到,ThreadLocalMap中的key是弱引用,其内部实现就体现在这里。

3.2.2. ThreadLocalMap的set方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

Entry[] tab = table;
int len = tab.length;
// 1. 定位Hash桶位置
int i = key.threadLocalHashCode & (len-1);

// 2. 从当前桶位置往下遍历
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {

ThreadLocal<?> k = e.get();

// 3. 如果key相同,替换掉原来的value
if (k == key) {
e.value = value;
return;
}

// 4. 如果当前桶位置key为null,特殊处理:替换并清除过期Entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

// 5. 找到一个为null的桶,将传入的Entry放入当前桶
tab[i] = new Entry(key, value);


int sz = ++size;

// 6. 没有清理出可用的桶而且容量超过阈值,重新Hash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

set方法,作用很明显,跟HashMap的put方法一样,就是将<key, value>键值对放入map。当然,考虑到会有Hash冲突,所以需要特殊处理(处理方式前文已经介绍)。

详细过程已经在源码中附加了注释,其中的『注释4』(当前位置key为null)和『注释6』是跟ThreadLocal的内存泄露相关的,我们将在『ThreadLocal的内存泄露』章节介绍到。

3.2.3. ThreadLocalMap的getEntry方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
// 1. 定位Hash桶位置
int i = key.threadLocalHashCode & (table.length - 1);

// 2. 获取当前桶位置的Entry
Entry e = table[i];

// 3. 如果Entry不为null且可以相同,说明Hash命中,返回对应的值即可
if (e != null && e.get() == key)
return e;

// 4. 未命中,特殊处理
else
return getEntryAfterMiss(key, i, e);
}

get方法的作用,也无需多说,跟HashMap的get方法一样,根据key去找value。同样,考虑到Hash冲突,会有未命中的情况,需要做特殊处理,即调用getEntryAfterMiss方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

// 1. 从当前位置往下找,这个原因在Hash冲突处理章节已经介绍过:
// 插入时,如果当前位置已经有元素,则往下找一个位置,看是否为null,
// 如此反复,直到找到一个为null的位置,把Entry放入该位置
// 所以,查找的时候,也是一样的逻辑,如果根据key算出来的Hash值位置上,不是当前Entry,
// 那么就顺着数组往下找
while (e != null) {
ThreadLocal<?> k = e.get();
// 2. 如果key相同,说明找到,直接返回
if (k == key)
return e;
// 3. 如果当前位置key为null,特殊处理:清除过期的Entry
if (k == null)
expungeStaleEntry(i);

// 4. 继续找数组的下一个位置
else
i = nextIndex(i, len);
e = tab[i];
}

// 最后还是没有找到,返回null
return null;
}

同样,这里需要特别注意的是『注释3』:当前数组位置key为null的情况,也是跟内存泄露相关的,『ThreadLocal的内存泄露』章节介绍到。

3.2.4. ThreadLocalMap的remove方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;

// 1. 定位Hash桶位置
int i = key.threadLocalHashCode & (len-1);

// 2. 遍历找出key对应的Entry,
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 3. 找到对应的Entry后,清理该Entry的弱引用
e.clear();
// 4. 特殊处理:清除过期的Entry
expungeStaleEntry(i);
return;
}
}
}

remove方法,根据key去删除map中的元素。这一过程中的特殊处理,也是跟内存泄露相关,会在内存泄露章节介绍。

4. ThreadLocal的内存泄露

4.1. 内存泄露原理分析

我们再回过头来看看ThreadLocal的底层实现:
QHAaGt.png

在 ThreadLocal 的生命周期中,都存在这些引用。如下图:实线代表强引用,虚线代表弱引用。
QHABM8.png

ThreadLocal 的实现是这样的:每个 Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。

也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

还记得文章一开始提出的ThreadLocal设计方案问题吗?我们设想的一种方案是:在ThreadLocal当中维护一个map,key为Thread,value为值。这和JDK采用的方案有什么差别呢,JDK为什么要这样做?话说在jdk1.3之前就是用这种方式做的,但是之后就改成了现在的这种做法。
这样做法的优点之一是,value放在了线程当中,随着线程的生命周期生存,线程死亡,value回收。之二是性能提高了,想想一下在有很多线程的应用中,如果按照之前的做法,map该多大?性能应该会比较低,而换成后者这种方法,map的大小变得比较小,和Threadlocal的数量相同(有多少个ThreadLocal,线程当中的map实际存储的就有多少个)。

ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key,如果一个 ThreadLocal 没有外部强引用来引用它,那么系统 GC 的时候,这个 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话(典型情况就是线程池方式,线程并真正不结束,只是归还到线程池中),这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

导致value永远无法回收,造成内存泄漏。

总的来说就是,ThreadLocal 里面使用了一个存在弱引用的 map,map 的类型是ThreadLocal.ThreadLocalMap. Map中的 key 为一个 threadlocal 实例。这个 Map 的确使用了弱引用,不过弱引用只是针对 key。每个 key 都弱引用指向 threadlocal。 当把 threadlocal 实例置为 null 以后,没有任何强引用指向 threadlocal 实例,所以 threadlocal 将会被 gc 回收。

但是,我们的 value 却不能回收,而且这块 value 无法被访问到了(因为key被回收了),所以存在着内存泄露。因为存在一条从current thread连接过来的强引用。只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread、Map value 将全部被 GC 回收。

4.2. 内存泄露问题解决

上文已经详细介绍了ThreadLocal中内存泄露的原因。那么是不是说明我们使用ThreadLocal就一定会有内存泄露问题呢?答案是否定的。否则的话,那这就是JDK的一个重大bug,ThreadLocal也就没有存在的必要了。

那么JDK是如何去解决这个内存泄露问题的呢?不知道大家还记不记得,在上文分析ThreadLocalMap API源码的过程中,特意留了几个略带思考的问题:

在ThreadLocalMap的set、getEntry、remove方法中,都提到了『特殊处理』

这个『特殊处理』就是为了解决内存泄露问题,它会清理掉不再被使用的Entry、value对象。

4.2.1. ThreadLocalMap的清理

在介绍内存泄露问题解决方案之前,我们先来看一个比较重要的基础:ThreadLocalMap的清理。

ThreadLocalMap特殊的Hash冲突处理方式,导致了:

清理 ThreadLocalMap 时候要保证将一个 index 指向的 Slot 清理之后,需要连带着将挨着该 index 的非空 Slot 内的 ThreadLocal 对象全部 Rehash 一遍。

因为这些 Slot 内存储的 ThreadLocal 对象和 index 指向的 Slot 内存储的 ThreadLocal 对象都 Hash 到了同一个 ThreadLocalMap 内的 Slot,如果把开头 Slot 清理后面的不 Rehash 就无法找到他们了(这一过程详见ThreadLocalMap的get方法)。

JDK源码中,执行清理 ThreadLocalMap 的操作的有三个地方:

  • 主动调用 ThreadLocalMap 内的 remove
  • set 值到 ThreadLocalMap 时调用 replaceStaleEntry 和 cleanSomeSlots
  • getEntry 时如果发现 Key 找不到会执行 expungeStaleEntry

下面将详细介绍这三种清理机制。

4.2.2. ThreadLocalMap 的 remove

同样,我们还是来看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;

// 1. 定位Hash桶位置
int i = key.threadLocalHashCode & (len-1);

// 2. 遍历找出key对应的Entry,
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 3. 找到对应的Entry后,清理该Entry的弱引用
e.clear();
// 4. 特殊处理:清除过期的Entry
expungeStaleEntry(i);
return;
}
}
}

其中的清理工作就是在 expungeStaleEntry 方法中执行的。我们来看看这个神秘的expungeStaleEntry 方法。

4.2.3. expungeStaleEntry源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 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;

// 1. 清理当前slot的Entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

// Rehash until we encounter null
Entry e;
int i;

// 2. 从当前位置开始往下遍历,直到找到为null的slot
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// key为null,该Entry已经无法被访问,清除改Entry
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// key不为null,需要rehash,原因在『ThreadLocalMap的清理』小节讲过
int h = k.threadLocalHashCode & (len - 1);

// 如果h==i,说明当前ThreadLocal与清理开始的ThreadLocal不是同一个run,不需要处理
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;
}

在 ThreadLocal 代码中,被 Hash 到同一个 Slot 的所有 ThreadLocal 对象称为一个 run,他们在 ThreadLocalMap 数组内紧挨着存储。

expungeStaleEntry的工作是传入一个 Slot 的 index,将该 index 指向的 Slot 清理,并且将该 index 之后同一个 run 内的所有 Slot 都检查一遍,发现 Slot 指向的 ThreadLocal 被 GC 则也清理该 Slot,没 GC 就将该 ThreadLocal 对象重新 rehash 到 ThreadLocalMap 的其它 Slot 上。最终会返回目标 index 所在 run 的终点序号,也即一个 run 末尾的空 Slot 的 index 值。

4.2.4. ThreadLocalMap 的 set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

Entry[] tab = table;
int len = tab.length;
// 1. 定位Hash桶位置
int i = key.threadLocalHashCode & (len-1);

// 2. 从当前桶位置往下遍历
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {

ThreadLocal<?> k = e.get();

// 3. 如果key相同,替换掉原来的value
if (k == key) {
e.value = value;
return;
}

// 4. 如果当前桶位置key为null,特殊处理:替换并清除过期Entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

// 5. 找到一个为null的桶,将传入的Entry放入当前桶
tab[i] = new Entry(key, value);


int sz = ++size;

// 6. 没有清理出可用的桶而且容量超过阈值,重新Hash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

set 操作是传入一个 ThreadLocal 对象和待和其绑定的 value,将这个 ThreadLocal 和 value 存入 ThreadLocalMap 中。存的时候也是需要先对 ThreadLocal 对象做 Hash 找到其在 ThreadLocalMap 中的 Slot,如果 Slot 被占用,会有三种情况:

  • Slot 内存储的 ThreadLocal 对象就是当前待存储的 ThreadLocal 对象,此时只需要用新 Value 替换原来的 Value 就结束了;
  • Slot 内存储的 ThreadLocal 不是当前待存储的 ThreadLocal 对象,并且之前存的 ThreadLocal 对象已经被 GC 掉,Slot 内 Entry 的 WeakReference 读取后返回空,这种情况下需要将原来 Entry 废弃并建立新的 Entry 指向这个新的 ThreadLocal 对象,存入当前的 Slot。这个替换过程使用的是 replaceStaleEntry 方法;
  • 如果不是上面两种情况,则需要继续查看紧挨着的 Slot 直到遇到空 Slot。找到空 Slot 说明我们找到一个空位置,则创建全新的 Entry 指向当前 ThreadLocal 对象,存入这个找到的空 Slot;

如果是上面第三种情况,添加完新的 Entry 之后,还会执行一次 cleanSomeSlots 方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* Heuristically scan some cells looking for stale entries.
* This is invoked when either a new element is added, or
* another stale one has been expunged. It performs a
* logarithmic number of scans, as a balance between no
* scanning (fast but retains garbage) and a number of scans
* proportional to number of elements, that would find all
* garbage but would cause some insertions to take O(n) time.
*
* @param i a position known NOT to hold a stale entry. The
* scan starts at the element after i.
*
* @param n scan control: {@code log2(n)} cells are scanned,
* unless a stale entry is found, in which case
* {@code log2(table.length)-1} additional cells are scanned.
* When called from insertions, this parameter is the number
* of elements, but when from replaceStaleEntry, it is the
* table length. (Note: all this could be changed to be either
* more or less aggressive by weighting n instead of just
* using straight log n. But this version is simple, fast, and
* seems to work well.)
*
* @return true if any stale entries have been removed.
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
// 1. 清理当前已经失效的Entry
if (e != null && e.get() == null) {
n = len;
removed = true;
// 2. 清理过期的Entry,前文讲过
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);

return removed;
}

是在当前新添加的 Entry 所在 Slot 之后,连续的找 log N 个 Slot,判断这些 Slot 内存储的 Entry 是否指向一个已经被 GC 的 ThreadLocal 对象,是的话就对这个 Slot 执行 expungeStaleEntry,做清理。其中 N 是当前 ThreadLocalMap 内存储的 ThreadLocal 对象总数。

对于上面第 2 种情况中使用的 replaceStaleEntry 其实现还比较复杂,拿下图来说:
QHAyZQ.png

假设当前要存的是 ThreadLocalB,并且 ThreadLocalA B C 在这个 ThreadLocalMap 都具有相同的 Hash 值,从而都 Hash 到同一个 Slot 即现在 ThreadLocalA 所在的 Slot。也正因为碰撞所以 ThreadLocalB C 都是紧挨着 ThreadLocalA 存储的。3 号位 Slot 指向 null 表示它本来是存一个 ThreadLocal 对象的,但这个对象被 GC 了,所以按照上面对 set 方法的描述,再次 set ThreadLocalB 的时候发现 3 号位是 null 就会执行 replaceStaleEntry,希望将 3 号位 replace 为 ThreadLocalB 并绑定上最新的 Value。

但是因为我们只检查到 3 号位,我们只能确认 2 3 两个位置没有 ThreadLocalB 对象,但 ThreadLocalB 对象可能存在于 3 号位之后的 Slot 中,所以直接将 ThreadLocalB 存入 3 号位是不行的,需要从 3 号位向后遍历着查找一下看看 3 号位之后还有没有 ThreadLocalB 对象了,如上图所示 3 号位之后还确实是有 ThreadLocalB 对象,并且因为发现 3 号位原来的 ThreadLocal 对象已经被 GC,所以 replaceStaleEntry 需要将 4 号位的 ThreadLocalB 挪到 3 号位,并且将该 ThreadLocalB 对象绑定上新的 Value。交换之后 4 号位我们知道是需要被清理的,所以会调用 expungeStaleEntry 将该位置的 Slot 清理,并且将 4 号位之后的 Slot 都进行 rehash。

当前面 expungeStaleEntry 执行之后,还是会调用 cleanSomeSlots 来探测当前 run 之后,也即 6 号位 Slot 之后 log N 个 Slot 看看有没有被 GC 掉的 ThreadLocal,有的话就用 expungeStaleEntry 做清理。

需要注意的是如果在 4 号位找到 ThreadLocalB,则 4 号位之后是不可能再有 ThreadLocalB 的,所以找到 4 号位做完交换和更新 Value 之后不需要从 4 号位再往后找有没有 ThreadLocalB 了。

除了上面说的这一大堆之外,replaceStaleEntry 实际还会检查同一个 run 内 3 号位之前的 Slot,看看这些 Slot 的 ThreadLocal 对象有没有被 GC 掉,虽然这些 Slot 在 replaceStaleEntry 执行之前,在 set 方法内已经检查过一次。从 replaceStaleEntry 内注释来看主要原因是想避免连续的 rehash。我个人推测,主要是因为 set 操作三种情况中,最耗时的就是第二种需要执行 replaceStaleEntry 的情况,无论是直接找到被更新的 ThreadLocal 对象直接更新绑定的 Value 还是在一个 run 内没有发现被 GC 的 ThreadLocal 对象直接将新的 ThreadLocal 存在一个 run 的末尾的空 Slot 内,耗时都是比较小的,而需要执行 replaceStaleEntry 时因为清理一个 Slot 需要将后续所有 Slot 全部 Rehash 所以耗时最大,所以要尽可能的避免 replaceStaleEntry 的执行。而 GC 是任意时刻都可能执行的,虽然 set 操作内检查过上图 2 号位,但是 GC 过后可能 2 号位的 ThreadLocalA 也被 GC 掉了,所以再次检查一下能更好的避免 replaceStaleEntry 的执行。

如果发现 3 号位之前有 ThreadLocal 对象被 GC,则在替换完 3 号位后,会直接从 3 号位之前这个被 GC 的 ThreadLocal 对象所在 Slot 开始,完整的执行一遍 expungeStaleEntry,全部执行完后相当于是从 expungeStaleEntry 执行开始的 Slot 到一个 run 的末尾所有被 GC 掉的 ThreadLocal 都会被清理。

replaceStaleEntry方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
* Replace a stale entry encountered during a set operation
* with an entry for the specified key. The value passed in
* the value parameter is stored in the entry, whether or not
* an entry already exists for the specified key.
*
* As a side effect, this method expunges all stale entries in the
* "run" containing the stale entry. (A run is a sequence of entries
* between two null slots.)
*
* @param key the key
* @param value the value to be associated with key
* @param staleSlot index of the first stale entry encountered while
* searching for key.
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;

// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;

// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();

// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;

tab[i] = tab[staleSlot];
tab[staleSlot] = e;

// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}

// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}

// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);

// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

4.2.5. ThreadLocalMap的getEntry

ThreadLocalMap的getEntry方法,在未命中时,会调用getEntryAfterMiss,该方法也会做一次清理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

// 1. 从当前位置往下找,这个原因在Hash冲突处理章节已经介绍过:
// 插入时,如果当前位置已经有元素,则往下找一个位置,看是否为null,
// 如此反复,直到找到一个为null的位置,把Entry放入该位置
// 所以,查找的时候,也是一样的逻辑,如果根据key算出来的Hash值位置上,不是当前Entry,
// 那么就顺着数组往下找
while (e != null) {
ThreadLocal<?> k = e.get();
// 2. 如果key相同,说明找到,直接返回
if (k == key)
return e;
// 3. 如果当前位置key为null,特殊处理:清除过期的Entry
if (k == null)
expungeStaleEntry(i);

// 4. 继续找数组的下一个位置
else
i = nextIndex(i, len);
e = tab[i];
}

// 最后还是没有找到,返回null
return null;
}

在key为null时,会调用expungeStaleEntry方法进行清理,前文已经分析过expungeStaleEntry过程,不再赘述。

4.2.6. 内存泄露问题小结

到此为止,ThreadLocalMap的三种清理全部讲述完毕,分别是:

  • 主动调用 ThreadLocalMap 内的 remove
  • set 值到 ThreadLocalMap 时调用 replaceStaleEntry 和 cleanSomeSlots
  • getEntry 时如果发现 Key 找不到会执行 expungeStaleEntry

所以,大多数情况下,使用ThreadLocal不会产生内存泄露问题,因为在后续的set、get过程中,ThreadLocal会自动进行内存清理。

ThreadLocal 自动清理机制需要依赖于用户调用 ThreadLocalMap 下的 set 和 getEntry 两个方法,即ThreadLocal的set、get方法,如果一个 ThreadLocal 对象已经被 GC,用户不再向同一个 Thread 绑定新的 ThreadLocal 对象,也再不读取 Thread 上的其它 ThreadLocal 对象,就无法触发 ThreadLocalMap 的 set 和 getEntry 方法,导致 ThreadLocal 内存储的 Value 对象永久驻留内存。

所以即使ThreadLocal有自动内存清理机制,依然建议使用remove方法来手动清理内存。使用完ThreadLocal变量后,手动remove是个非常好的习惯!

5. 思考题

啰里啰嗦写了将近1万字,也不知道列位看官有多少收获,这里列几个思考题供列位看官思考下:

  1. 既然ThreadLocalMap的key用弱引用来帮助GC,那么value是否也可以使用弱引用?这样不就不存在内存泄露问题了吗?
  2. ThreadLocalMap为什么要采用开放地址法,而不是像HashMap那样,采用链地址法(即数组+链表)?

6. 参考