ThreadLocal原理和内存泄露分析

ThreadLocal是用户可以描述为基于线程绑定的共享变量,在平常使用时使用静态变量加上对应的get,set方法进行调用。

另外大量的开发中我们使用Slf4j的MDC工具类来替换直接使用ThreadLocal,效果很不错。

1
2
3
4
5
6
7
8
9
10
11
public class A {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void set(String value){
threadLocal.set(value);
}
public static String get() {
return threadLocal.get();
}
}

那么我们关心的问题来了。

  1. 作为一个绑定在线程上的变量,这种静态变量调用方式究竟是如何做到的呢?
  2. 作为整个线程级别的共享变量,是如何进行内存管理的,会不会存在内存泄露?

针对这两个问题,简单描述下,重点还是通过源码来进行分析。

ThreadLocal有一个静态内部类 ThreadLocalMap,该对象以Map结尾就可以想象到这是一个集合容器,并且使用KV结构进行内部的存储,然而并不是Map的实现。

ThreadLocalMap并没有在ThreadLocal中使用到,所以这个类一定在其他地方被用到,既然是线程级别的共享变量,这个类是不是在Thread对象中呢?答对啦,在Thread中定义了ThreadLocalMap变量,我们称之为资源变量。

在一个常用的Web开发中,通常在Filter、Request、Connection等场景用到ThreadLocal对象,也就是资源变量中会存在若干个ThreadLocal,从KV接口可以很好推算出,ThreadLocalMap是个资源变量池,可以使用ThreadLocal作为key,最终拿到ThreadLocal对应的value,而事实也就是这么设计的。

从这里可以理解为,ThreadLocal是Thread和资源变量中的中间桥梁。

在阅读源码之前,我的理解上,既然ThreadLocal作为Map的key,ThreadLocal类本身一定需要重写equals和hashCode,否则会引起碰撞陷阱,然后ThreadLocal却并没有这么实现,取而代之的是重写了一个变量作为hashCode来解决key的碰撞问题.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

ThreadLocalMap是如何实现KV数据结构呢,这里用的是一个数组

1
2
3
4
5
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;

下面重点关注一下Entry这个类,第二个问题关于内存管理重点就在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

Entry是一个弱引用对象,并且引用的是ThreadLocal对象,究竟这里是如何进行内存管理呢?

先明确一个基础概念,存在A,B 2个对象,A和B之间可以存在4中引用关系,针对弱引用而言,还需要定义一个C去链接A和B对象,程序实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A
class B
private C c;
class C extend WeakRefrence<A>
class Test {
public static main(String[] args){
C c = new C(new A);
B b = new B();
b.c = c;
System.gc();
}
}

B强引用A的引用对象C,而A是用过弱引用关联到C,所以在gc时,A对象会被gc掉。

用这个基础去套Entry该如何解释呢?这里简单重写下ThreadLocal,ThreadLocalMap对象关系。

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
public class ThreadLocalMap {
/**
* entry的Key用weakReference封装
* value为entry只有的强引用。
*/
private Entry[] table;
/**
* 记录添加的值,方便编列内容
*/
private int modCount=0;
/**
* 模拟ThreadLocal.ThreadLocalMap的set动作。
*/
public void put(BigByte bigByte , byte[] value){
if( table == null ){
table = new Entry[10];
}
Entry entry = new Entry( bigByte , value);
table[modCount] = entry;
modCount++;
}
public void printAll(){
for( int i = 0 ; i< modCount ; i++ ){
System.out.println( "--------------------------------" );
System.out.println( "entry:" + table[i] );
System.out.println( "key:" + table[i].getKey() );
System.out.println( "value:" + table[i].getValue() );
System.out.println( "--------------------------------" );
}
}
}
public class ThreadLocal {
private ThreadLocalMap tm = new ThreadLocalMap();//模拟ThreadLocalMap。
public void set(BigByte bigByte , byte[] value){
tm.put(bigByte, value);
}
public ThreadLocalMap getTm() {
return tm;
}
}
public class Entry<BigByte> extends WeakReference<BigByte>{
private byte[] value;
public Entry(BigByte bigByte , byte[] value) {
super(bigByte);
this.value = value;
}
public BigByte getKey(){
return this.get();
}
public byte[] getValue(){
return this.value;
}
}
public class BigByte {
private byte[] bigByte ;
public BigByte(){
this.bigByte = new byte[1024*1024*10];//1M
}
//定义一个key的值,作为table的参考
}
//WeakReferenceCar关联的对象Car被回收掉了,注意是弱引用关联的对象car被回收,而不是弱引用本身wrc被回收
public static void main(String[] args) throws InterruptedException {
BigByte k1= new BigByte(); //外部强引用
Thread thread = new Thread(() -> {
ThreadLocal th = new ThreadLocal();//自己模拟的ThreadLocal 里面持有一个ThreadLocalMap对象
//唯一不同的是set方法内部是put进新的值,而Java的ThreadLocal是"set" 老值会被新值替换。
th.set( k1 , new byte[1024*1024*50] );//50M put 进50M
//th.set( new BigByte() , new byte[1024*1024*50] );//50M put 进50M
th.set( new BigByte() , new byte[1024*1024*50] );//50M put 进50M
th.set( new BigByte() , new byte[1024*1024*50] );//50M put 进50M
ThreadLocalMap tm = th.getTm();
tm.printAll();//打印entry和key,value 方便观察
System.out.println("####################");
th = null;//将ThreadLocal置为null还是会有内存泄露,并且FULL GC 不会回收
System.gc();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
tm.printAll();//打印entry和key,value 方便观察
});
thread.start();
TimeUnit.SECONDS.sleep(5);
System.gc();
}

这里模拟了一个线程周期内和线程结束时,通过gc回收弱引用对象的过程。
执行看些结果:

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
[GC (Allocation Failure) 127540K->113376K(177664K), 0.0383301 secs]
--------------------------------
entry:me.j360.jdk7.refence.Entry@1f7a85de
key:me.j360.jdk7.refence.BigByte@11bd6841
value:[B@1582eeae
--------------------------------
--------------------------------
entry:me.j360.jdk7.refence.Entry@5080627a
key:null
value:[B@1b9f49c1
--------------------------------
--------------------------------
entry:me.j360.jdk7.refence.Entry@619ad593
key:me.j360.jdk7.refence.BigByte@636fa090
value:[B@30af9d35
--------------------------------
####################
[GC (System.gc()) 175481K->164496K(262656K), 0.0025963 secs]
[Full GC (System.gc()) 164496K->164416K(262656K), 0.0128830 secs]
--------------------------------
entry:me.j360.jdk7.refence.Entry@1f7a85de
key:me.j360.jdk7.refence.BigByte@11bd6841
value:[B@1582eeae
--------------------------------
--------------------------------
entry:me.j360.jdk7.refence.Entry@5080627a
key:null
value:[B@1b9f49c1
--------------------------------
--------------------------------
entry:me.j360.jdk7.refence.Entry@619ad593
key:null
value:[B@30af9d35
--------------------------------
[GC (System.gc()) 167079K->164416K(262656K), 0.0030230 secs]
[Full GC (System.gc()) 164416K->10815K(262656K), 0.0530523 secs]

可以得到资源对象其实就是Entry中的value,在整个Thread生命周期内是不会被回收的,但是作为弱引用引用的Key却在每次gc都会被回收,然后这种回收并不会影响Thread去拿到对应的数据,究竟是如何实现的?

1
2
3
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

在Thread类中还存在一个固定的对象,以Thread.currentThead作为参数去重新生产ThreadLocal作为Key,也就是ThreadLocal对象时刻都可以被回收,既不会引起泄露也不会影响资源变量的获取,这里的重要函数非setInitialValue莫属了。

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
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();
}
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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;
}
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

通常在Map中存在一种key为null的情况,在ThreadLocalMap的代码中都进行了判断,也就是说ThreadLocal作为key都会被进行非null判断。

然后。。。

if在判断null并不能代表 Entry.get()方法能够拿到ThreadLocal对象,因为在if刚刚好判断完就产生了gc吧ThreadLocal给回收了,在ThreadLocalMap中的代码,在使用Entry.get()时已经考虑到了这种因素,不再详细描述。