JDK源码分析——ThreadLocal

引:可能大家对ThreadLocal这个类既熟悉又陌生,看到得多用到得少。

详细注释:源码分析地址

概览

该类提供了线程局部 (thread-local) 变量。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)

应用:在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个Connection对象。由于JDBC的连接对象不一定是线程安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将JDBC的连接保存到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
public class ThreadLocalTest {

public static void main(String[] args) {
ThreadLocal<String> myThreadLocal = new MyThreadLocal<String>();

// 输出leonard
System.out.println(myThreadLocal.get());

myThreadLocal.set("rex");
// 输出rex
System.out.println(myThreadLocal.get());

myThreadLocal.remove();
// 输出leonard
System.out.println(myThreadLocal.get());
}

}

class MyThreadLocal<T> extends ThreadLocal<T> {
@Override
protected T initialValue() {
return (T) "leonard";
}
}

get

返回线程局部变量的当前线程副本中的值。如果当前线程中保存的变量副本没有值,则先将调用 initialValue() 方法初始化然后返回值。

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
73
74
75
76
77
78
79
80
public T get() {
// 拿到当前线程
Thread t = Thread.currentThread();
// 拿到 ThreadLocalMap 这个Map
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;
}
}
// 不存在这样的map,则设置初始化,然后返回值
return setInitialValue();
}

// 拿到与ThreadLocal关联的Map
ThreadLocalMap getMap(Thread t) {
// 与当前线程绑定
return t.threadLocals;
}
// ThreadLocal的数据结构是一个HashMap
// 节点是继承了 WeakReference(弱引用)
// 如果一个对象具有弱引用,在GC线程扫描内存区域的过程中,不管当前内存空间足够与否,都会回收内存
// 这个我们要思考一下为什么用弱引用?
// 想象一下,如果你的ThreadLocal在局部变量中使用,而且没有remove
// 那么这个ThreadLocal对应的value永远不会被回收,加上引用更像是防御编程
// 我们关注getEntry方法
private Entry getEntry(ThreadLocal<?> key) {
// 拿到数组下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// Hash值没有冲突
if (e != null && e.get() == key)
return e;
// Hash值冲突
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) {
// 拿到节点对应的ThreadLocal(key) 弱引用
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
// 清理数组
expungeStaleEntry(i);
// 存储到下一个下标里(处理Hash冲突采用的是线性探测法)
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

// 设置初始化的值,然后返回
private T setInitialValue() {
// 拿到初始值
T value = initialValue();
// 拿到当前线程
Thread t = Thread.currentThread();
// 拿到当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// Map已经存在
if (map != null)
// 设置ThreadLocal对应的值
map.set(this, value);
// Map不存在,创建Map
else
createMap(t, value);
// 返回该值
return value;
}

Set

将线程局部变量在当前线程副本中的值设置为指定值。大部分子类不需要重写此方法,它们只依靠 initialValue() 方法来设置线程局部变量的值。

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
public void set(T value) {
// 拿到当前线程
Thread t = Thread.currentThread();
// 拿到当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果Map不为空,则直接设置值
if (map != null)
map.set(this, value);
// 如果Map为空,则创建Map,然后设置值
else
createMap(t, value);
}

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

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 设置初始容量,默认16
table = new Entry[INITIAL_CAPACITY];
// 找到下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 设置值
table[i] = new Entry(firstKey, firstValue);
// 设置size
size = 1;
// 设置阈值
setThreshold(INITIAL_CAPACITY);
}

private void setThreshold(int len) {
// 设置阈值为 16 * 2 / 3 = 10
threshold = len * 2 / 3;
}

我们再来看看Map的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
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
private void set(ThreadLocal<?> key, Object value) {

Entry[] tab = table;
int len = tab.length;
// 根据 ThreadLocal 的 HashCode 得到对应的下标
int i = key.threadLocalHashCode & (len-1);
// 首先通过下标找对应的entry对象
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 改变其值
if (k == key) {
e.value = value;
return;
}
// 如果key被 GC 回收了,就会变为null(因为是软引用),则创建一个新的 entry 对象填充该槽
if (k == null) {
// 这个方法可以好好看看
replaceStaleEntry(key, value, i);
return;
}
}
// 对象不存在,则创建一个新的 entry对象
tab[i] = new Entry(key, value);
// size + 1
int sz = ++size;
// 如果没有清除多余的entry 并且数组长度达到了阀值,则扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 扩容
rehash();
}

private void rehash() {
// 清楚陈旧的节点
expungeStaleEntries();

// Use lower threshold for doubling to avoid hysteresis
// 如果size大于0.75倍阈值。原来是2/3, 则负载因子相当于为0.5,这是在第一次的时候
// 在首次扩容之后,负载因子还是0.75
if (size >= threshold - threshold / 4)
resize();
}

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

romove

移除此线程局部变量在当前线程的值。

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
public void remove() {
// 得到当前线程的Map
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

// 调用ThreadLocalMap的remove方法
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 通过线性探测法找到 key 对应的 entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 将ThreadLocal设置为null
e.clear();
// 清理所有的 key 为 null 的 entry
expungeStaleEntry(i);
return;
}
}
}

总结

ThreadLocal无限好,但是也会有问题,比如内存泄漏的问题(可以在后面的参考理解这个问题),为了防止这个问题,我们需要每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

参考

  1. 并发编程之 ThreadLocal 源码剖析
  2. 深入分析 ThreadLocal 内存泄漏问题
  3. 使用ThreadLocal不当可能会导致内存泄露