最近在阅读一些框架的源码时经常看到ThreadLocal的使用,于是通过阅读、分析源码进行学习。

先来官方对ThreadLocal的定义

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).

翻译过来大致是说

ThreadLocal类提供线程局部变量,这些变量与其常见的应对方式不同,因为在每个线程中可以通过get或set方法去访问属于线程自己的独立初始化的变量副本。ThreadLocal实例通常在类中以私有静态字段呈现,来将状态和线程进行关联。

翻译过来非常拗口,我自己的理解是对象在不同的线程之间往往是共享的,而ThreadLocal提供了一种隔离机制,它允许我们创建一个只能够由同一个线程进行读写的变量,且该变量对其他线程不可见。

下面开始分析JDK是如何实现ThreadLocal的,以下源码分析基于jdk1.8.0_66

ThreadLocal类的结构如下
ThreadLocal

先来看get方法

1
2
3
4
5
6
7
8
9
10
11
12
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

首先获取当前的Thread对象

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

getMap方法获取到Thread对象的成员变量ThreadLocalMap,再来看这个类的实现。
ThreadLocalMapThreadLocal的一个内部类,来看其部分代码实现

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
static class ThreadLocalMap {

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

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

private static final int INITIAL_CAPACITY = 16;

private Entry[] table;

private int size = 0;

private int threshold; // Default to 0

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

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

可以看到ThreadLocalMap是一个以Entry数组模拟实现的Map,key是ThreadLocal对象。其为数组的原因是我们可能会在一个类中创建多个ThreadLocal对象,而非一个。另外需要指出的是,Entry继承了WeakReference来降低发生内存泄露的可能性(自己对Java各种Reference并不是十分清楚,接下来会写一篇博客来记录下学习的体会,先挖个坑)。

由此可以得出结论,变量实际存储在Thread中而非ThreadLocal中,每一个Thread都有自己的ThreadLocalMap变量,这样一来各个Thread之间互不干扰,没有线程安全问题,从而避免了多个线程同时操作同一个Map而需要加锁的问题,提高了性能。

继续看get方法,如果获取到ThreadLocalMap,那么通过ThreadLocal对象作为key获取到Entry对象,从而可以取到我们设置的值。如果没有获取到ThreadLocalMap,则调用setInitialValue方法来设置一个初始值

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

protected T initialValue() {
return null;
}

然而,initialValue方法的默认实现返回null,如果我们要让其返回其他东西,则需要去重写这个方法。

接下来看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);
}

同样的方式去获取ThreadLocalMap对象,如果其不存在,那么为该线程创建一个ThreadLocalMap对象,

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

如果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
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)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

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

private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

可以看到,set方法对Entry数组进行遍历,解决Hash冲突的方式是数组下标自增1的方式去寻找下一个位置。
如果该位置的key和目标的key相等,则直接覆盖value,如果该位置的Entry存在但是key为null,则将key和value进行赋值。
如果找到了一个位置但这个位置的还没有Entry对象,则会新建一个Entry对象并赋值,并进行空间清理(清理key为nullvalue)、视情况进行再散列。

最后看一下remove方法,

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

同样的方法获取到该线程的ThreadLocalMap成员变量,调用其remove方法,参数为ThreadLocal对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}

同样的方式遍历Entry数组,找到key对应的Entry对象并清除。

总结一下,ThreadLocal的核心实际上是他的内部类ThreadLocalMap,而其作为Thread类的成员变量,承担了所谓线程局部变量的存储的工作,setget等各种操作都在ThreadLocalMap上进行,由于每个线程都有自己的ThreadLocalMap变量,因此各个线程之间互不干扰,变量也互不可见,这样的设计也很好的避免了多个线程操作同一个Map的问题。在进行源码的阅读分析之后,我的感受是ThreadLocalMap相对于ThreadLocal本身,它的复杂度要高不少。

我认为ThreadLocal的应用场景是每个线程都有其相对应的实例对象,且这个对象在程序的其他地方需要频繁的使用到,从而可以避免或降低变量传递的复杂度,如sessionThreadLocal不是用来解决线程之间资源共享的问题的,恰恰相反,它是用来解决资源隔离的问题的。资源共享的问题还是需要依靠同步或锁来解决。

另外值得我们警惕的是,ThreadLocal使用不当有可能会造成内存泄露,根本原因是如今往往都会使用线程池,这样一来线程结束后没有被销毁。具体分析可以参考下面的两篇文章:

深入分析 ThreadLocal 内存泄漏问题
ThreadLocal可能引起的内存泄露