ThreadLocal的工作原理

Q:为何说可以通过ThreadLocal实现在同一进程内维护同一数据在不同线程中各自的值并且互不干扰?

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ThreadLocal<Boolean> mBoolThreadLocal = new ThreadLocal<Boolean>();
//in the mainThread:
mBoolThreadLocal.set(true);
//打印mBoolThreadLocal.get();
new Thread("thread_1"){
@ovveride
public void run(){
mBoolThreadLocal.set(false);
//打印mBoolThreadLocal.get();
}
}
new Thread("thread_2"){
@override
public void run(){
//打印mBoolThreadLocal.get()
}
}

从上面的三个线程分别打印结果为:主线程为true,thread_1为false,thread_2为null(为null的原因是在线程2中并没有mBoolThreadLocal的备份)。

分析其中的原因要从ThreadLocal源码中找原因。
首先每一个线程的内部都持有一个ThreadLocal.ThreadLocalMap对象(Key(ThreadLocal)->Value(CopyObj)), 以ThreadLocal作为key, 是因为一个线程内可以new出很多个ThreadLocal对象, 每一个ThreadLocal对象都会对应一个备份Obj的值, 而每一个ThreadLocalMap则缓存了当前线程所有的备份Objs. 需要理解的是一个ThreadLocal对应一个CopyObj, 一个线程内可以创建多个ThreadLocal对象。

//源码分析的部分我觉得我写的不够好,一位小米工程师写的很好了所以我直接贴上他的回答吧~
解密ThreadLocal

——-以上的博文分析的版本有更新

thread-localMap结构已经被替换成Valeus容器,虽然这个容器依旧起的是一个映射的作用却不是一个真正的map结构,实际上是一个Object[]数组结构->table。而table的尺寸是等于entry的两倍。一个entry占据两个table[]的单位,前者存放ThreadLocal,后者存放value。因为table的length初始值为16*2而每次的扩容收缩都是移位操作故table.length一定是2的幂。
ThreadLocal.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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}
//Values.put()
void put(ThreadLocal<?> key, Object value) {
cleanUp();//在put一个value前先清理
// firstTombstone是添加new entry的位置
int firstTombstone = -1;
for (int index = key.hash & mask;; index = next(index)) {
Object k = table[index];
if (k == key.reference) {
// Replace existing entry.
table[index + 1] = value;
return;
}
//第一次fill threadlocal.value
if (k == null) {
if (firstTombstone == -1) {
// Fill in null slot.
table[index] = key.reference;
table[index + 1] = value;
size++;
return;
}
// Go back and replace first tombstone.
table[firstTombstone] = key.reference;
table[firstTombstone + 1] = value;
tombstones--;//使用tombstones作为新的填充slot
size++;
return;
}
// Remember first tombstone.
if (firstTombstone == -1 && k == TOMBSTONE) {
firstTombstone = index;
}
}
}
//Values.cleanUp():
private void cleanUp() {
if (rehash()) {
//如果需要执行rehash操作就没必要执行clean Up操作了。
return;
}
//容器内啥都没clean个毛啊
if (size == 0) {
return;
}
// Clean log(table.length) entries picking up where we left off
// last time.
int index = clean;
Object[] table = this.table;
for (int counter = table.length; counter > 0; counter >>= 1,
index = next(index)) {
Object k = table[index];
if (k == TOMBSTONE || k == null) {
continue; // on to next entry
}
// The table can only contain null, tombstones and references.
Reference<ThreadLocal<?>> reference
= (Reference<ThreadLocal<?>>) k;
if (reference.get() == null) {
// This thread local was reclaimed by the garbage collector.
table[index] = TOMBSTONE;
table[index + 1] = null;
tombstones++;
size--;
}
}
// Point cursor to next index.
clean = index;
}
//values.rehash()
//在必需扩张或者收缩arrays的时候rehash,在每一次填入一个null slot的时候都需要rehash 为啥呢?
//每一次的搜索都是靠检索到null slot作为结束搜索的标志,如果不执行rehash搜索行为将无限循环
private boolean rehash() {
if (tombstones + size < maximumLoad) {//最大的负载量就是当前容量的2/3
return false;
}
int capacity = table.length >> 1;
int newCapacity = capacity;
//如果当前的live entry数量大于容积的一半 那么就执行扩容吧
//没有的话就把现有的entry移到另外一个等大小的Object[]数组中
//逐个检查每一个entry是否可以被回收掉,类似一次gc操作。
if (size > (capacity >> 1)) {
// More than 1/2 filled w/ live entries.
// Double size.
newCapacity = capacity * 2;
}
Object[] oldTable = this.table;
//创建新的table
initializeTable(newCapacity);
// We won't have any tombstones after this.
this.tombstones = 0;
if (size == 0) {
return true;
}
// 遍历entry然后逐个检查
for (int i = oldTable.length - 2; i >= 0; i -= 2) {
Object k = oldTable[i];
if (k == null || k == TOMBSTONE) {
continue;
}
Reference<ThreadLocal<?>> reference
= (Reference<ThreadLocal<?>>) k;
ThreadLocal<?> key = reference.get();
if (key != null) {
//key不为空说明这个threadlocal还是有引用到的,就移动到新的table里面去
add(key, oldTable[i + 1]);
} else {
// The key was reclaimed.
size--;
}
}
return true;
}

综上,每当一个thread对自己的ThreadLocal.Values对象进行put(Object value)操作时,首先会去遍历存储表中是否含有空的位置,如果有空位置标记为TOMBSTONE,此标记可让下一次fill对象的时候直接填进来,而不需要占用新的位置。当然在执行clean之前还会进行一次rehash判断,检查当前的live entry数量是否大于capacity/2。是的话直接执行一次扩容操作(期间已经清理了TOMBSTONE的空间就不需要继续去clean了)