ThreadLocal 源码解读

缘起

【1】中我们已经见识到了在框架代码中大量使用ThreadLocal变量. 遂激发了我想了解ThreadLocal的源码的兴趣.

分析

形象的说(虽然源码实现并不是这样):

ThreadLocal\ 可以视作 Map<Thread, T>

也就是其实ThreadLocal变量使得我们可以实现线程隔离——针对每根线程存储了各自具体的,每根线程各拿各的,就像商场里面的存包柜子一样——既然每根线程拿的东西都不一样,自然就不会存在并发问题. 所以线程隔离也是实现无锁的一种方式(都不存在竞争,要锁干什么呢?). 线程终止之后,这个会被gc掉.

ps: 实现线程安全的手段有 “有锁”、”无锁”, 其中有锁就是同步,无锁有 线程封闭+不变性两种,其中线程封闭又分为ad hoc、栈限制、和本文的ThreadLocal 这三种. 具体可以参见【2】,ThreadLocal是线程封闭较好的实现方式.

在分析源码之前,列举一些ThreadLocal的应用场景

数据库连接

让每根线程拥有自己的数据库连接,即

1
2
3
4
5
6
7
8
9
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
}

public static Connection getConnection() {
return connectionHolder.get();
}

其实JDBC规范是没有要求java.sql.Connection 接口的实现是线程安全的. 就是因为可以使用上述ThreadLocal的手段隔离各个线程. 所以没必要线程安全.

频繁的分配写缓存(buffer)

例如一种写操作很频繁,而每次都要获取buffer进行写操作. 此时就可以考虑将buffer放进ThreadLocal中去.

如果使用共享全局变量的话,显然是需要使用锁进行同步的——不能让两次写操作并发使用该共享buffer. 但是如果使用ThreadLocal的话,就不存在竞争,也就不需要加锁. 效率较高. 但是比较尴尬的事情是——除非分配的极为频繁或者一次分配的代价极为高昂. 来一次写操作就分配一次或许更好. 也就是在写操作并不是那么的频繁的情况下ThreadLocal并不能带来多少性能优势.

应用程序框架

这个其实用的最为广泛——将上下文(例如运行的参数)缓存到一个静态的ThreadLocal中去, 则框架代码想知道当前线程中的信息的时候,就可以从ThreadLocal中获取. ThreadLocal的缺点就是容易使得业务代码和框架代码耦合,即框架的侵入性会提高.

列举了上面的应用场景,我们开始分析ThreadLocal的源码

首先要说明一下ThreadLocal的简单用法

假设 我们新建了2个ThreadLocal变量

1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadLocalHolder {
private static ThreadLocal<User> threadLocal_1 = new ThreadLocal<>();
private static ThreadLocal<Client> threadLocal_2 = new ThreadLocal<>();

public static set_1(Object v) {
threadLocal_1.set(v);
}

public static set_2(Object v) {
threadLocal_2.set(v);
}
}

并且在三根线程中使用他们

1
2
3
4
5
6
7
8
9
10
// thread-1中
ThreadLocalHolder.set_1(user_1);
ThreadLocalHolder.set_2(client_1);

// thread-2中
ThreadLocalHolder.set_1(user_2);
ThreadLocalHolder.set_2(client_2);

// thread-3中
ThreadLocalHolder.set_2(client_3);

则最终,会是下图这个样子

​ 图1

为什么呢?

我们来看看ThreadLocal.set方法

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

​ 源码1

第二行首先得到当前线程(currentThread是native方法,在此不做过多讨论),第三行的源码如下

ThreadLocal.getMap

1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

​ 源码2

可见,我们其实是获取当前线程的一个名为threadLocals的属性

1
ThreadLocal.ThreadLocalMap threadLocals = null;

​ 源码3

注意,这个属性是默认访问权限的, 也就是不准备暴露给别的包下的类(当然,除非你在项目中也新建一个java.lang的包),所幸, ThreadLocal和Thread都是java.lang包下的, 所以ThreadLocal中可以访问到该Thread的属性. 回到源码1,就知道了,我们其实是对Thread的这个threadLocals属性进行操作,而这个threadLocals其实是一个类似Map的东西——ThreadLocalMap(即图1中的代表每根线程的圆角框框里面的矩形), 我们先不管这个东西到底是什么,我们目前就知道我们使用ThreadLocal进行set的时候其实是对这个map进行set(以ThreadLocal变量本身为key),后面我们会再细讲这个ThreadLocalMap的api源码.

不过,在此之前,我们先对ThreadLocalMap有一个大概的印象——来自ThreadLocalMap的官方注释

1
2
3
4
5
6
ThreadLocalMap is a customized hash map suitable only for
maintaining thread local values. No operations are exported
outside of the ThreadLocal class. The class is package private to
allow declaration of fields in class Thread. To help deal with
very large and long-lived usages, the hash table entries use
WeakReferences for keys.

即ThreadLocalMap是仅仅Thread内部用的一个Map(事实上,ThreadLocalMap就是ThreadLocal的私有内部类)——它的实现和java.util.HashMap一样也是通过哈希表(其实就是一个Entry数组,但是ThreadLocal的Entry是java.lang.ThreadLocal.ThreadLocalMap.Entry,和java.util.HashMap的java.util.Map.Entry<K, V> 不同)实现的,但是java.util.HashMap处理哈希冲突的方法是链表,而这里的ThreadLocalMap处理哈希冲突的是方法是开发地址法之线性探测. 之所以采用这个,完全是因为ThreadLocalMap对它的哈希算法的自信, 这个后面会再细说. 而且为了处理长时间存在的线程(例如,线程本身就是从线程池中获取的,线程将长久存在而不消失),这里的哈希表的元素——Entry使用的是弱引用(即Entry extends WeakReference<ThreadLocal<?>>). 这个后面会细说.

同理, ThreadLocal.get() 方法源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}

​ 源码4

发现没有,也是一样的套路——都是先获取当前线程,然后找到这根线程中的ThreadLocalMap属性,然后直接以ThreadLocal变量为key,从该map中获取value. 并且返回。这就好像是一根线程Thread中执行ThreadLocal变量.get()的话,ThreadLocal变量会告诉当前线程Thread——“嘿,伙计,其实你要的东西就藏在你自己身体里(即Thread中的ThreadLocalMap属性),你只需要拿我当key,去你自己体内的这个ThreadLocalMap中去找就行了”. 换言之, ThreadLocal本身不存储数据,数据都是在各自Thread中(的ThreadLocalMap属性)维护的.

如果map属性是null的话(Thread类中对此属性的初始化就是null),则调用源码4的第12行,跟进去

ThreadLocal.setInitialValue

1
2
3
4
5
6
7
8
9
10
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;
}

​ 源码5

套路依旧——获取当前线程,获取线程的ThreadLocalMap属性,如果是null的话(第一次就是null),就跟进源码5的第8行. 但是在跟进之前,我们来看看第2行,这里挺有意思,这里是获取ThreadLocal的初始化值的. 跟进去

ThreadLocal.initialValue

1
2
3
protected T initialValue() {
return null;
}

​ 源码6

注意,貌似是返回null,但是这个方法是protected的,所以如果使用ThreadLocal的时候Override了这个方法的话,返回的就不是null了. 也就是经常我们能看到的如下用法

1
2
3
4
5
6
private static final ThreadLocal<XXX> threadLocal = new ThreadLocal<>() {
@Override
protected Object initialValue() {
return ZZZ; // 该ThreadLocal的初始化值是ZZZ
}
};

好了,经过此小插曲,我们来到源码5的第八行

ThreadLocal.createMap

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

​ 源码7

跟进如下构造器

1
2
3
4
5
6
7
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { // firstValue就是上述initialValue的返回值
table = new Entry[INITIAL_CAPACITY]; // 初始化table(一个Entry数组),初始化容量是16
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 计算firstValue就是上述initialValue的返回值的哈希值
table[i] = new Entry(firstKey, firstValue);
size = 1; // table包含的初始entry数量是1
setThreshold(INITIAL_CAPACITY); // 常量INITIAL_CAPACITY=16
}

​ 源码8

这段代码有一些要说的. 从简单的说起,首先就是第6行——setThreshold,顾名思义就是设置table数组的阈值(因为哈希表的扩容是基操),扩容操作源码如下

ThreadLocalMap.setThreshold

1
2
3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}

​ 源码9

即阈值是table数组的容量的2/3, 其中table是ThreadLocal的属性. 下面来看源码8的第三行. 其实这种& 运算本质就是对table数组的长度取模, 这种操作在分析HashMap的源码的时候已经说过了(详见【4】的源码3的哈希1). 这里要说的是源码8第三行的threadLocalHashCode,它是ThreadLocal的属性——threadLocalHashCode

1
private final int threadLocalHashCode = nextHashCode();

​ 源码10

注意,它的初始化是通过以下函数进行的

ThreadLocal.nextHashCode

1
2
3
4
5
6
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647; /// 一个神奇的数(1640531527)——实现完美哈希

​ 源码11

因为源码10 的threadLocalHashCode不是静态属性,所以源码11的顺序可以随意. 这是java的基础知识. 就不多说了, 而且源码10一旦获取到了threadLocalHashCode之后,每次调用ThreadLocal实例的threadLocalHashCode属性都不会变的.

其中源码11的第二行就实现了完美哈希. 所谓完美哈希指的是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static void hashCode(Integer length){
int hashCode = 0;
for(int i=0;i<length;i++){
hashCode = i*HASH_INCREMENT+HASH_INCREMENT;//每次递增HASH_INCREMENT
System.out.print(hashCode & (length-1));//求散列下标,算法公式
System.out.print(" ");
}
System.out.println();
}
public static void main(String[] args) {
hashCode(16);//初始化16
hashCode(32);//后续2倍扩容
hashCode(64);
}
/*
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 --》Entry[]初始化容量为16时,元素完美散列
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0--》Entry[]容量扩容2倍=32时,元素完美散列
7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0 --》Entry[]容量扩容2倍=64时,元素完美散列
此算法(即源码11中的nextHashCode方法)在长度为2的N次方的数组上,确实可以完美散列(至于机制,涉及较多的数学理论,这里不展开了)
*/

正因为如此,ThreadLocalMap的table属性的官方注释是

1
The table, resized as necessary. table.length MUST always be a power of two.

即table的长度(注意,table的长度length和ThreadLocalMap另一个属性size——The number of entries in the table. 不是同一个值,并且显然有size <= length)

好了,我们再次回到源码8的第四行,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;
}
}

​ 源码12

注意,Entry是弱引用? 关于java中的四大引用详见【3】. 为什么要使用弱引用呢? 弱引用的一大特点就是如果有强引用对象引用着对象(一块堆内存),则GC的时候宁可抛OOM也是不会GC的,但是如果一个对象只被一个弱引用对象引用着的话,一旦遇到GC的话,就会被GC。为了解释为什么必须是弱引用,如果使用的Entry是ThreadLocal变量的强引用,例如Entry的代码这样写

1
2
3
4
5
6
7
8
9
10
11
static class Entry  {
/** The value associated with this ThreadLocal. */
Object value;

ThreadLocal<?> t; // 持有ThreadLocal的强引用

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

我们想想,如果使用了一个ThreadLocal变量t(其实t是ThreadLocal的引用对象,只是一个引用而已)

1
ThreadLocal<?> t = new ThreadLocal<>();

但是事后觉得不需要了,就直接将该ThreadLocal变量的引用置空

1
t = null;

那么你觉得下次GC的时候,原本t指向的堆内存会被GC吗? 显然不会,因为如果线程长时间存在(例如它是从线程池中获取的线程,线程不停止,ThreadLocalMap不释放的,何以为证? 我们看看Thread的源码就知道了

Thread.exit()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null; // 释放ThreadLocalMap
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}

​ 源码13

源码13的第九行足以说明Thread和它的ThreadLocalMap同岁齐辉~

),则它的ThreadLocalMap属性就会长时间存在,则ThreadLocalMap的Entry数组, 即table属性就会长时间存在——table强引用着这个ThreadLocal堆内存呢! 导致即便你在应用程序中显式置空了ThreadLocal变量——t=null, 但是t指向的堆内存依旧不会被GC。这就是堆内存泄漏了. 为了解决这个问题,使用了弱引用对象——Entry实例去引用ThreadLocal堆内存. 一旦置t为空(null),则因为没有强引用t罩着了,唯余Entry这个弱引用,则下次GC的时候,ThreadLocal堆内存就会被回收. 其实这种思想在【5】(WeakHashMap)中也用到了. 都是为了解决这种问题的,或者说,弱引用就是为此而生的.

那么,如果使用了弱引用了,就万事大吉了吗? 非也非也~ 还是存在内存泄漏问题.

WTF? 我没听错吧? 刚刚说了半天,使用弱引用解决的问题不就是为了解决内存泄漏问题吗? 怎么还有? 事实是,ThreadLocal变量对应的堆内存的内存泄漏问题是被解决了,ThreadLocal只是Entry的键,但是Entry的值的内存泄漏问题又蹦出来了. 怎么讲? 因为Entry的键(ThreadLocal)只是弱引用,一旦显式t=null置空之后,很可能在某次GC中会回收,则此Entry的键变成null, 但是应用程序是不可能利用null进行查询的, 但是此Entry的value会不会被GC掉呢? 同之前的道理也是不会的,因为Thread长时间存在–>ThreadLocalMap长时间存在–>table数组长时间存在–>强引用着Entry元素(的value), 这就造成了Entry的值的内存泄漏(之前通过弱引用解决的是Entry键的内存泄漏问题,现在又来了值的内存泄漏问题). 怎么解决呢? JVM团队当然注意到了这个问题,Josh Bloch 和 Doug Lea两位大神(ThreadLocal的作者)的解决办法是:每次对ThreadLocalMap进行get/set/remove 的时候,对其table数组中非空但是键空的Entry(即table[i]!=null && table[i].get()==null, 注意,get不是Entry自己的方法,而是Entry的父类——java.lang.ref.Reference的get方法, 根据源码12就知道table[i](即Entry实例)的get方法返回的就是k, 即键),我们称这种Entry 为脏项(网上也都这么叫),进行清除.

再次强调:脏项是怎么形成的? 就是通过直接去除ThreadLocal变量的强引用,导致在各个线程的ThreadLocalMap中的table数组(即哈希表)中相应的Entry的键被回收(变null),但是值没有被回收,即值所占据的堆内存空间出现了内存泄漏. 这种情况就叫做脏项.

ps: 网上说的很多ThreadLocal内存泄漏问题其实就是这种脏项的出现,但是如我们所见,JVM团队已经出手尽可能的解决这个问题了.

下面就来细看源码,这两位大神是如何实现的,Sorry, ThreadLocal讲了半天,其实是在了解它的背景(我的一贯认知就是,一个东西的历史背景其实往往比它本身更重要,很多时候,你看不懂一个东西,有时候未必是你的单纯知识积累不够,而是你不了解它诞生的历史背景, 所以前面的讲解其实一样重要).

回到ThreadLocal的set方法. 即源码1,我们主要看源码1的第五行. 跟进去

java.lang.ThreadLocal.ThreadLocalMap.set(ThreadLocal<?>, Object)

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
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;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) { // 因为哈希的充分,所以其实此for循环走的不多
ThreadLocal<?> k = e.get();

if (k == key) { // 如果找到了key(显然不是脏项),则简单覆盖
e.value = value;
return;
}

if (k == null) { // 遇到了脏项,但是未必后面就不会出现此key(因为完全可能先遇到脏项), 所以才要进到replaceStaleEntry方法中去而不能简单这里就将脏项赋予key/value,否则如果简单赋予替换掉的话,哈希表中可能出现两个key(一个是这里简单但是错误的替换掉的,另一个就是后面暂时没遇到的),这岂不是糟了?
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

​ 源码14

第12行的for循环显然是在处理哈希冲突. 其中i 是插入的key(即ThreadLocal)本来应该占据的索引. 如果没冲突的话(所谓没冲突就是for循环中一直没有return, 即哈希冲突也只是没有脏项,而且键和ThreadLocal不相等.),直接执行第28行,即table[i]插入该条目. 否则的话,第15行首先取得i处的已被占据的ThreadLocal,然后如果发现与待插入的ThreadLocal是同一个的话(比的是地址,不是equals方法),则第18行简单覆盖了事(即return). 如果发现被占据的是一枚脏项的话(即源码14的第22行),则要做清除脏项的处理——replaceStaleEntry,注意,Stale的意思就是陈旧的,腐烂的,也就是这里说的脏.

处理完脏项之后(这里面的处理一定包含将待插入的键值插入table), 直接返回. 我们先不细跟replaceStaleEntry方法,只知道此方法的三个入参分别是待插入的键值和本应该插入的索引i(注意,i可能因为table[i]不是脏项但是其键也不等于key而通过nextIndex方法而变化,nextIndex方法仅仅是一个简单的线性探测的方法,说白了就是往后加1, 这里仅仅列出源码而不分析了(后面也有类似的prevIndex方法,和这里类似).

java.lang.ThreadLocal.ThreadLocalMap.nextIndex(int, int)

1
2
3
4
5
6
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

). 如果for循环中一直没有return的话,也就是哈希冲突一直是”真的”哈希冲突, 即冲突项也不是脏项. 那么就执行源码14的第30行,进行cleanSomeSlots调用,它的入参是table[i]为null的i(因为你来到了这里,只可能是table[i]为null), 以及当前table数组中的条目(或者叫项)的个数. 注意,之前说的源码14的第23行的replaceStaleEntry里面也是需要调用cleanSomeSlots的,所以打算一起说. 源码14的第30行大概你留个印象说的就是如果table中的真实包含的条目的个数(size)超过了阈值(ThreadLocalMap的threshold属性,在源码8的第六行初始化)并且在table[i]附近没发现脏项的话,则就要进行重哈希. rehash方法后面我们细说.

这里进入本文的重头戏——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
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) { // staleSlot后续有key,则交换
e.value = value;

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

if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); // expungeStaleEntry(slotToExpunge)出发,清除脏项
return;
}

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);
}

​ 源码15

这里要记得从源码14第23行的入参——key和value就是待插入的键值对(键是一个ThreadLocal强引用),staleSlot是脏项在table数组中的索引(这个位置是在源码14的第23行原本准备插入key、value的,但是现在进到replaceStaleEntry方法中来看看后面是不是有key,因为你总不可能一个哈希表中有两个一样的key吧,源码14的第22行也解释的很清楚为什么需要进到此方法中来了). 源码15第12行之前,我们使用slotToExpunge保存了往前寻找脏项的索引,寻找的方式是只要table项为null的话,就停止寻找,如果不为null的话,看看是不是脏项,如果是的话,更新slotToExpunge,注意哦, slotToExpunge有可能和入参的staleSlot相等哦~ 然后开始进行源码15的第13行——向后寻找,寻找的方式也是一样的——如果找到了null的table项的话,就停止. 每考察一项,即源码15的第15行,如果和当前需要插入的键相等的话,则表明哈希表中其实是有这个key的,只是源码14的第22行我们先遇到脏项了而已. 但是这个时候,有的读者可能就会有问题——如果在源码15第16行,即遇到哈希表中的key之前就遇到了null呢? 这是一个问题诶~ 因为这样就会导致源码15的第12行的for循环提前结束,导致的后果就是——我们再也碰不到key了诶~ 但是这种担心是多余的~ 为什么? 因为后面要讲解的cleanSomeSlots中真正起到删除作用的expungeStaleEntry调用中会进行重哈希的,进而不会造成这里说的情况的. 这个expungeStaleEntry方法我们后面会再讲.

回到源码15第17行,即我们发现了key(即e=table[i].get()=key),则将table[i]应该移动到脏项staleSlot的位置处(因为原本table[i]肯定是出于处理哈希碰撞的原因才来到了table[i],现在占着它原本应该占据的table[staleSlot]的元素变成了脏项,则table[i]这个非脏项自然可以回归咯~),所以源码15 的第17~20行就做了这件事情. 然后如果往前找脏项未果的话(即slotToExpunge=staleSlot),则更新slotToExpunge=i(即table[i], 它现在不是已经和之前的table[staleSlot]在源码15 的17~20行交换了么? 所以table[i]变成了脏项,而table[staleSlot]迎接非脏项),其实slotToExpunge这个东西就是下一步马上就要进行的清除脏项的起点索引, 所以如果之前往前找脏项如果找到了的话,则显然这里不能将清除脏项的起点更新为i. 然后源码15 的第24行就开始清除脏项了——start from “expungeStaleEntry(slotToExpunge)”, 这个expungeStaleEntry后面我们马上就讲. 然后就返回了,也就是如果源码15找到了key的话,就交换,然后清理脏项. 然后终止for循环, 返回.

如果寻找的过程中一直没找到key, 但是找到了脏项(即源码15的第28行),并且从staleSlot往前没找到脏项(所以slotToExpunge=staleSlot没变过),则就更新脏项的起点slotToExpunge=i(于是,这是清除脏项的新起点,后面for循环再找到脏项,因为slotToExpunge不再等于staleSlot而不会更新了).

如果for循环执行完而没有因为找到了key而return的话,则来到了源码15第33行,说明key就是在哈希表中本就不存在,注意,staleSlot在整个replaceStaleEntry过程中是不变的,一直保持入参的样子, 所以直接插入即可,即源码15的33~34行做的事情. 最后, 源码15 的第37行,如果slotToExpunge != staleSlot,表明整个过程中虽然没找到key,但是发现了脏项,则就需要清除脏项. 和源码15的第24行一样,调用的是cleanSomeSlots(expungeStaleEntry(slotToExpunge), len), 下面我们来细讲这个调用.

首先,来看expungeStaleEntry(slotToExpunge)这个调用. 入参slotToExpunge是脏项的起点. 即table[slotToExpunge] 是脏项. 跟进去

java.lang.ThreadLocal.ThreadLocalMap.expungeStaleEntry(int)

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
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;
}

​ 源码16

牢记源码16 的入参是一个脏项的索引(其实是源码15决定的脏项的起点——slotToExpunge). 所以源码16 的第6到第八行做的事情就是”expunge entry at staleSlot”. 注意,这里的清除就体现了前面说的JVM团队解决因为弱引用导致的ThreadLocalMap的Entry值的内存泄漏问题. 因为这里直接去掉了值的强引用(Bytab[staleSlot].value = null). 并且将整个table[staleSlot]置空了.

然后要做的事情就是”Rehash until we encounter null”(即expungeStaleEntry不仅仅清除的是一个脏项,它比较热心肠——还将附近的脏项也统统一网打尽了),源码16 的第17行,如果遇到了脏项的话,则和6~8行一样清除它,如果不是脏项的话,就需要重哈希了. 这里重哈希的方法是,首先计算它理想的位置h(源码16 的第22行),然后如果和当前位置不符的话才进行下面的——将现在的位置tab[i]置空,并且利用开放地址法之线性探测找到新的位置. 注意,重哈希的意义何在? 我至少发现2点,第一点是哈希表进行重哈希利于下一次搜索的速度,这一点属于哈希表的共识,第二点就是为了避免前面讨论过的”有的读者可能就会有问题…”

好了,最后源码16返回的东西就是table[i]=nulll的索引,因为源码16 的for循环中没有提前return的语句啊.

弄懂了expungeStaleEntry,我们回到源码15 的此方法的两处调用,一处是源码15的24行,一处是38行. 这下子我们就知道了expungeStaleEntry(slotToExpunge)作为入参是什么了. 就是从slotToExpunge开始咣咣咣一顿处理(脏项移除,非脏项重哈希)之后为null的table索引. 跟进cleanSomeSlots方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}

​ 源码17

记住,cleanSomeSlots的入参,i是一个table[i]=null的索引i, n是table数组的length. 返回的removed就是如果一旦发生了清除,则就是返回true,否则如果从未发生过清除脏项的话,则返回的就是false. 注意,首先解释一下为什么这里有个 n>>>1, 这是保符号右移. 其实官方的注释也说清楚了

1
2
3
4
5
6
7
8
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.
译文
当新元素被添加时,或者另一个过期元素已被删除时,会调用cleanSomeSlots。该方法会试探性地扫描一些 entry 寻找过期的条目。它执行对数数量的扫描,是一种 基于不扫描(快速但保留垃圾)和 所有元素扫描之间的平衡。

如果发现了脏项table[i](源码17 的第八行),则置n为table数组的长度(相当于是小时候打游戏的回血续命),标记removed为true(即的确成功的删除了脏项),然后最后调用expungeStaleEntry(这个源码16细说过了),对脏项table[i]以及附近的脏项进行清除, 最后源码17的第11行的i就是一个table[i]为 null(进而不是脏项)的索引.

好了,至此replaceStaleEntry已经分析清楚了. 我们回到源码14第23 行,即如果发现脏项,就replaceStaleEntry了事返回. 如果源码14的for循环一直没碰到脏项,也没有找到key的话,则来到源码14的28行,——直接插入<key, value> 到table[i]了,并且将table数组中包含Entry的数目+1, 然后源码14的第30行又调用了cleanSomeSlots,i就是刚刚插入<key,value>的索引. 注意,table[i]是刚刚插入的<key,value>, 所以肯定不是脏项. 所以cleanSomeSlots的第一个入参是一个非脏项的索引,回忆源码15 的24行和38行cleanSomeSlots的入参是table[i]=null(所以肯定也不是脏项),所以cleanSomeSlots对它的第一个入参的官方注释就是

1
a position known NOT to hold a stale entry. The scan starts at the element after i.

因为铁定不是脏项,这就是为什么源码17,即cleanSomeSlots的源码第6行上来就获取i后面的元素的原因了——因为table[i]肯定不是脏项嘛~ 因为cleanSomeSlots的源码已经细说过了,所以源码14的第30行说的就是如果在table[i]直接插入<key, value>的过程中没发现脏项并且已经超出阈值的话,就要扩容并且重哈希,即我们走进源码14 的第31行的rehash方法.

java.lang.ThreadLocal.ThreadLocalMap.rehash()

1
2
3
4
5
6
7
private void rehash() {
expungeStaleEntries();

// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}

​ 源码18

首先进行的是

java.lang.ThreadLocal.ThreadLocalMap.expungeStaleEntries()

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Expunge all stale entries in the table.
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null) // 如果e是脏项
expungeStaleEntry(j);
}
}

​ 源码19

注意!这会儿是动真格的了! 不再是仅仅是附近的,而是如注释所说——“Expunge all stale entries in the table.”.

算法很简单——就是一旦发现脏项,就调用expungeStaleEntry进行清除.

然后源码18的第5行就是说如果当前table中拥有的条目数>=3/4*阈值,注意,阈值初始是table的2/3,所以大概就是说如果table中的条目数>=table的长度的一半的话,则就会进行扩容,跟进resize方法

java.lang.ThreadLocal.ThreadLocalMap.resize()

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
/**
* Double the capacity of the table.
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;

for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) { // 处理掉脏项
e.value = null; // Help the GC,防止内存泄漏
} else {
int h = k.threadLocalHashCode & (newLen - 1); // 计算老数组中的元素在新数组中的索引
while (newTab[h] != null)
h = nextIndex(h, newLen); // 开放地址法之线性探测处理哈希冲突
newTab[h] = e;
count++;
}
}
}

setThreshold(newLen);
size = count;
table = newTab;
}

​ 源码20

resize扩容的源码很简单,就是将原数组的length扩倍,然后将原table数组中的东西搬到新数组中去,只不过第18行计算新的位置的时候用的是新数组的长度而不是老数组的长度(因为显然哈希要针对新数组进行嘛~)并且将老数组元素中的脏项释放掉它的value(防止内存泄漏)

好了,至此ThreadLocalMap中最为重要的set方法的源码已经解析完毕. 其他的方法的源码就变得简单起来. 我们来彻底解析一下ThreadLocal中的API

java.lang.ThreadLocal.get()

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}

​ 源码21

首先拿到Thread中的ThreadLocalMap,然后调用此ThreadLocalMap中的getEntry方法

java.lang.ThreadLocal.ThreadLocalMap.getEntry(ThreadLocal<?>)

1
2
3
4
5
6
7
8
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1); // 计算散列
Entry e = table[i];
if (e != null && e.get() == key) // 如果找到了,直接返回条目
return e;
else
return getEntryAfterMiss(key, i, e); // 如果没有命中,要么e是null,要么e.get()!=key(注意,可能是null,即是脏项)
}

​ 源码22

java.lang.ThreadLocal.ThreadLocalMap.getEntryAfterMiss(ThreadLocal<?>, int, Entry),其中入参e是鸠占鹊巢中的鸠,i是巢, key是鹊的key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null) // 如果是脏项
expungeStaleEntry(i); // 处理脏项,并且对于脏项后续的项——是脏项就处理,不是脏项就重哈希,i是脏项索引,处理掉了,则tab[i]就是null,除非在重哈希阶段tab[i]重新有了值
else
i = nextIndex(i, len); // 下一位
e = tab[i];
}
return null;
}

​ 源码23

java.lang.ThreadLocal.set(T)

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

​ 源码24

关于第5行,我们之前是详尽分析过了的(源码14),关于第七行,源码7也分析过了.

java.lang.ThreadLocal.remove()

1
2
3
4
5
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

​ 源码25

java.lang.ThreadLocal.ThreadLocalMap.remove(ThreadLocal<?>)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) { // 采用线性探测的方法找到key,然后调用expungeStaleEntry对其进行清除
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}

​ 源码26

其实说到这里,感觉挺搞笑的~ 为什么? 因为一般我们项目里面也不可能对ThreadLocal进行滥用,一般顶多就是1个,2个左右,而table初始化的容量就有16,基本上不会存在哈希冲突的可能性.

分析完ThreadLocal的源码之后,我们来分析一下ThreadLocal的子类——InheritableThreadLocal

1
public class InheritableThreadLocal<T> extends ThreadLocal<T>

它的作用是什么呢? 这就不得不从线程类的构造器说起

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
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}

Thread.init
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}

Thread.init
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) { // inheritThreadLocals传入的是true
if (name == null) {
throw new NullPointerException("name cannot be null");
}
...
Thread parent = currentThread();
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}

java.lang.Thread.currentThread()
public static native Thread currentThread();

​ 源码27

值得注意的是源码27的第23行,注意,inheritableThreadLocals是Thread的一个属性,它也是ThreadLocalMap类型的,和Thread的threadLocals属性一样. 但是不同的是,Thread的threadLocals属性不在Thread的构造器中初始化,而是在ThreadLocal的set方法中的createMap(源码1的第七行)被初始化的. 而Thread的inheritableThreadLocals是在Thread的构造器中初始化的. 跟进源码27的第23行

java.lang.ThreadLocal.createInheritedMap(ThreadLocalMap)

1
return new ThreadLocalMap(parentMap);

继续跟

java.lang.ThreadLocal.ThreadLocalMap.ThreadLocalMap(ThreadLocalMap)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];

for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) { // e是父线程的inheritableThreadLocals属性中的table数组的元素
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}

​ 源码28

其实源码28就是使用父线程的inheritableThreadLocals属性(源码27 的23行)对子线程的inheritableThreadLocals属性进行初始化.初始化的方法也就是遍历父线程的inheritableThreadLocals(一个ThreadLocalMap变量)的table数组(即哈希表),然后将其复制到子线程的inheritableThreadLocals(一个ThreadLocalMap)的哈希表(即table数组)中去. 但是复制的过程中并不是强制要求是完全复制的——允许子线程进行一定程度的定制. 定制之道就在于源码28的第13行. 即我们可以让我们的ThreadLocal变量是InheritableThreadLocal变量,然后覆写其中的childValue方法(ThreadLocal的childValue方法直接抛出UnsupportedOperationException异常,一种RuntimeException). 即可实现父线程(例如main线程)的值的定制. 而InheritableThreadLocal的set、get、remove方法其实都是调用其父类ThreadLocal的, InheritableThreadLocal仅仅覆写了以下三个关键方法

1
2
3
4
5
6
7
8
9
10
11
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) { // 意味着允许自定义子类进行覆写
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

​ 源码29

也就是说,对于InheritableThreadLocal而言,它的set、get方法和ThreadLocal不一样,后者操作的对象是Thread中的threadLocals属性,而前者操作的是Thread的inheritableThreadLocals属性.

说的比较抽象,参见下面的代码

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
package com.yfs.threadlocal;

import java.util.concurrent.TimeUnit;

/**
* 自定义InheritableThreadLocal
*
* @author yfs
*
*/
public class MyInheritableThreadLocal {

/** 线程本地遍变量 */
private static final InheritableThreadLocal<Integer> INHERITABLE_THREAD_LOCAL = new InheritableThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return null;
};

@Override
protected Integer childValue(Integer parentValue) {
return parentValue << 1; // Thread中的inheritableThreadLocals属性的初始化是乘以2
};

};

public static Integer get() {
return INHERITABLE_THREAD_LOCAL.get();
}

public static void set(Integer value) {
INHERITABLE_THREAD_LOCAL.set(value);
}

private static class MyIntegerTask implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "获取值: " + get());
}
}

public static void main(String[] args) throws InterruptedException {
new Thread(new MyIntegerTask()).start(); // 主线程中启动子线程1
TimeUnit.SECONDS.sleep(1); // 确保是在主线程往INHERITABLE_THREAD_LOCAL中塞入10之前启动的子线程1
set(10); // 主线程塞进10
TimeUnit.SECONDS.sleep(1); // 确保是在主线程往INHERITABLE_THREAD_LOCAL中塞入10之后启动的子线程2
new Thread(new MyIntegerTask()).start(); // 主线程中启动子线程2
}

}
/*
* 控制台打印
* Thread-0获取值: null
* Thread-1获取值: 20
* 这是因为,子线程2的inheritableThreadLocals属性初始化的时候,采用的是主线程(即父线程)的inheritableThreadLocals属性
* 而父线程的inheritableThreadLocals属性什么时候有值的呢? 就是在主线程调用set(10)的时候,其实是INHERITABLE_THREAD_LOCAL找到
* 主线程的inheritableThreadLocals属性,往里面塞进了<INHERITABLE_THREAD_LOCAL, 10> 这个键值对,则子线程2启动的时候
* 它的inheritableThreadLocals里面就是<INHERITABLE_THREAD_LOCAL, 20>了.
* 子线程1之所以是null,是因为它启动的时候,父线程的inheritableThreadLocals属性还是空呢~
*
*/

ThreadLocal常见的应用场景以及最佳实践

讲到了最后,我们来谈谈ThreadLocal在日常项目中的使用. 一般都是在项目中搞一个

1
2
3
4
public class XXXCache { // 或者叫XXXHolder

public static ThreadLocal<ZZZ> zzz = new ThreadLocal<>();
}

则我们在不同线程中就可以调用

XXXCache.set(一些参数),则可以在别的地方通过XXXCache.get() 获取到当前请求线程的这些参数. 十分方便的实现了线程隔离——支撑了高并发(因为无锁).

因为之前谈到了内存泄漏问题,所以ThreadLocal的最佳实践是——一旦当前线程(例如请求线程)使用完毕了该ThreadLocal变量,最好调用ThreadLocal.remove() 方法,即通知当前线程中它的ThreadLocalMap的哈希表table中移除该Entry中的键对应的value的强引用, 并且将此entry项的强引用去除. 这样就不会造成value的内存泄漏.

参考

【1】https://yfsyfs.github.io/2019/06/18/Spring-%E4%BA%8B%E5%8A%A1%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%EF%BC%88%E5%90%8E%E7%AF%87%EF%BC%89/

【2】https://yfsyfs.github.io/2019/06/27/java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E8%B7%B5-3-3-%E7%BA%BF%E7%A8%8B%E5%B0%81%E9%97%AD/

【3】https://yfsyfs.github.io/2019/06/28/Java%E4%B8%AD%E7%9A%84-%E5%BC%BA%E5%BC%95%E7%94%A8%E3%80%81%E8%BD%AF%E5%BC%95%E7%94%A8%E3%80%81%E5%BC%B1%E5%BC%95%E7%94%A8%E3%80%81%E8%99%9A%E5%BC%95%E7%94%A8/

【4】https://yfsyfs.github.io/2019/06/07/java-util-HashMap-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/

【5】https://yfsyfs.github.io/2019/06/28/WeakHashMap-%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB/