ThreadLocal解析

Posted by alonealice on 2016-12-07

ThreadLocal是一个关于创建线程局部变量的类,为解决多线程程序的并发问题提供了一种新的思路。使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。那它内部是如何实现的呢?
ThreadLocal对外提供三个方法:set、get和remove,那我们一次来看一下这几个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}

Values values(Thread current) {
return current.localValues;
}


Values initializeValues(Thread current) {
return current.localValues = new Values();
}

set方法很简洁,首先是获取当前的线程,再获取该线程的Values变量,如果线程的Values值为null,则创建新的Values给该Thread,然后将该ThreadLocal作为key,传入的值作为value,添加到Values中。那这个Values又是何方神圣呢。
Values是ThreadLocald的静态类,同时又是Thread的成员变量,它内部主要维护了一个Object数组。看一下它的构造方法:

1
2
3
4
5
Values() {
initializeTable(INITIAL_SIZE);
this.size = 0;
this.tombstones = 0;
}

这里有两个变量,一个是size,它表示的是数组中存储的values的数量,tombstones表示数组中无用值的数量,关于它,后面再说。接下来看initializeTable方法。

1
2
3
4
5
6
private void initializeTable(int capacity) {
this.table = new Object[capacity * 2];
this.mask = table.length - 1;
this.clean = 0;
this.maximumLoad = capacity * 2 / 3; // 2/3
}

首先,它会创建一个Size为传入数字的2倍的Object数组,记录数组的最大下标mask,设置清除的数组下标clean和最大的存储量maximumLoad。关于后面两个变量的用处,后面再说。接下栏看看它的put方法。

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
void put(ThreadLocal<?> key, Object value) {
cleanUp();

// Keep track of first tombstone. That's where we want to go back
// and add an entry if necessary.
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;
}

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--;
size++;
return;
}

// Remember first tombstone.
if (firstTombstone == -1 && k == TOMBSTONE) {
firstTombstone = index;
}
}
}

首先是清除无用的values,这个我们后面再说。接下来是根据ThreadLocal的hash值和之前数组的最大下标mask值,计算当前threadlocal在数组中的位置。取出当前位置的值,如果值为null,且之前位置没有被占用过,则说明该threadlocal在之前完全没有存储过,就在该数组位置存储key,该位置+1存储value;如果已经存储过,就循环找到该位置更新数据;如果位置被已清除的数据占用,且其他位置没有该数据,那就在被占的位置上添加数据。
从这里我们可以看到,threadlocal本身并不存储值,而是用Thread的values存储,threadlocal只是作为存储的key,而在数组存储的方式为两两并排存储,key在前,value在后。同时我们会发现几个问题,首先存储是threadlocal并不是直接将自己作为key存储,而是将自己的作为软引用来存储,其次,存储的可以会在什么时候被清除,那下面就来讲讲这两个问题。
首先来讲讲为什么不直接使用强引用作为key。当我们将threadlocal作为强引用key时,当引用的threadlocal对象要被回收时,由于该Thread的Values还持有ThreadLocal的强引用,导致这个threadlocal对象就不会被回收,从而导致了内存泄漏。而如果使用软引用时,由于Values持有的是ThreadLocal的软引用,ThreadLocal会被直接回收,这时只要在将那些已经被回收的key的value清除,就不会再造成内存泄漏了。当然这种方式依旧会有泄漏的风险,因为Thread的Values的生命周期跟Thread一样长,当在一个长生命周期的Thread(如mainThread)中没有及时的remove对象,且ThreadLocal被回收了,又没有再次执行set、或get方法,那么依旧会造成内存泄漏。所以要彻底的避免泄漏,还是要及时的清除无用的threadlocal。
上面说的,set和get方法会将那些已经被回收的threadlocal的值清除,那它又是怎么做的呢?这里有一个我们之前跳过的方法:cleanUp。那再来看看这个方法做了什么。

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
private void cleanUp() {
if (rehash()) {
// If we rehashed, we needn't clean up (clean up happens as
// a side effect).
return;
}

if (size == 0) {
// No live entries == nothing to clean.
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.
@SuppressWarnings("unchecked")
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数量不为空时,循环遍历数组。这里程序会记录之前检查过的下标为clean,这样可以减少循环次数。
当位置上的key没有没有被清除掉,且key的引用被清除掉时,会将该key和其对应的值都清掉。
接下来看看rehash方法,该方法用来判断是否需要扩大数组。

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
private boolean rehash() {
if (tombstones + size < maximumLoad) {
return false;
}
int capacity = table.length >> 1;
int newCapacity = capacity;

if (size > (capacity >> 1)) {
newCapacity = capacity * 2;
}

Object[] oldTable = this.table;

initializeTable(newCapacity);

this.tombstones = 0;

if (size == 0) {
return true;
}

for (int i = oldTable.length - 2; i >= 0; i -= 2) {
Object k = oldTable[i];
if (k == null || k == TOMBSTONE) {
continue;
}

@SuppressWarnings("unchecked")
Reference<ThreadLocal<?>> reference
= (Reference<ThreadLocal<?>>) k;
ThreadLocal<?> key = reference.get();
if (key != null) {
// Entry is still live. Move it over.
add(key, oldTable[i + 1]);
} else {
// The key was reclaimed.
size--;
}
}

return true;
}

这里我们看到,当所有的值数量(被清除的值+正常的值)大于最大的数量时,或者正常的值数量到达数组范围的一半时,都会重新设置数组。更详细的说,当被清除的值数量过大时,会重新设置从序号0开始检查,并不再将那些清除的值添加到新数组;当正常的值size过大时,会将数组扩大一倍,再讲值重新添加到数组。
然后我们再来看看get方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public T get() {
// Optimized for the fast path.
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values != null) {
Object[] table = values.table;
int index = hash & values.mask;
if (this.reference == table[index]) {
return (T) table[index + 1];
}
} else {
values = initializeValues(currentThread);
}

return (T) values.getAfterMiss(this);
}

首先会根据threadlocal获取数据在数组位置,从线程中获取数据。如果没有数据的话,就执行getAfterMiss方法:

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
Object getAfterMiss(ThreadLocal<?> key) {
Object[] table = this.table;
int index = key.hash & mask;

// If the first slot is empty, the search is over.
if (table[index] == null) { //1
Object value = key.initialValue();

// If the table is still the same and the slot is still empty...
if (this.table == table && table[index] == null) {
table[index] = key.reference;
table[index + 1] = value;
size++;

cleanUp();
return value;
}

// The table changed during initialValue().
put(key, value);
return value;
}

// Keep track of first tombstone. That's where we want to go back
// and add an entry if necessary.
int firstTombstone = -1;

// Continue search.
for (index = next(index);; index = next(index)) {
Object reference = table[index];
if (reference == key.reference) { //2
return table[index + 1];
}

// If no entry was found...
if (reference == null) {
Object value = key.initialValue();

// If the table is still the same...
if (this.table == table) {
// If we passed a tombstone and that slot still
// contains a tombstone...
if (firstTombstone > -1
&& table[firstTombstone] == TOMBSTONE) {
table[firstTombstone] = key.reference;
table[firstTombstone + 1] = value;
tombstones--;
size++;

// No need to clean up here. We aren't filling
// in a null slot.
return value;
}

// If this slot is still empty...
if (table[index] == null) {
table[index] = key.reference;
table[index + 1] = value;
size++;

cleanUp();
return value;
}
}

// The table changed during initialValue().
put(key, value);
return value;
}

if (firstTombstone == -1 && reference == TOMBSTONE) {
// Keep track of this tombstone so we can overwrite it.
firstTombstone = index;
}
}
}

该方法比较长,但是核心就几处。首先是1处,当位置上的key为null时,说明之前没有数据存储过,那就返回null,同时将null作为值添加到数组;其次是2处,从原位置+2处开始遍历循环数组,如果位置上的数据为key的reference,那就直接返回值;如果如果位置上的数据为不是key的reference,这说明之前的key被回收清除了,那么就将null作为值添加到该清除位置同时返回null。
最后再来看看remove方法:

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
public void remove() {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values != null) {
values.remove(this);
}
}
...
void remove(ThreadLocal<?> key) {
cleanUp();

for (int index = key.hash & mask;; index = next(index)) {
Object reference = table[index];

if (reference == key.reference) {
// Success!
table[index] = TOMBSTONE;
table[index + 1] = null;
tombstones++;
size--;
return;
}

if (reference == null) {
// No entry found.
return;
}
}
}

remove方法就比较简单了,如果key还存在,就将key和它的值都清除掉。
最后总结一下:首先ThreadLocal本身不存储任何值,它将自己作为key存储到线程的Values中;同时如果存储的位置已经有其他值,那它会增加序号到新的位置存储;如果存储的位置的值被清除了,且后面没有存储该值,那就会在该清除位置上储存。同时,每次get、set和remove方法,都会将那些已经被回收的key和值都清除掉。