ThreadLocal是什么?

ThreadLocal是线程之间进行协作和共享的重要工具,可以保证并发中的线程安全。

当我们定义好了ThreadLocal变量后,每个线程就会持有这个变量的副本,各个线程就可以单独操作这些变量而不会互相影响,当然我们也可以使用锁的方式来代替,用ThreadLocal这种方案不过是用了空间来换时间了。

一句话,ThreadLocal可以避免并发场景下的线程安全问题,因为你访问ThreadLocal变量实际操作的是自己线程中保存的副本。

ThreadLocal的使用

举个例子,在main线程中开启五个线程,每个线程都去修改 express对象 中的ThreadLocal变量的值,我们来看看是个什么情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Express {
//定义 ThreadLocal 变量,并将它的值初始化成100
ThreadLocal<Integer> tl = new ThreadLocal<Integer>() {
//重写 initialVlaue() 方法,将其赋值100
@Override
protected Integer initialValue() {
return 100;
}
};

public Express(){}

public void changeThreadLocalVariable(int num) {
//set() 方法,用来设置 ThreadLocal 的值
tl.set(num);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test {
//静态变量 express
private static final Express express = new Express();
public static class A extends Thread {
@Override
public void run() {
//以每个 Thread 的 id 作为值,重新设置当前线程中的 ThreadLocal 变量副本的值
int id = (int) Thread.currentThread().getId();
//副本设值
express.changeThreadLocalVariable(id);
System.out.println(Thread.currentThread().getName() + "修改后的值为:" + express.tl.get());
}
}

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
//以此启动五个线程
new A().start();
}
System.out.println("num实际的值为:" + express.tl.get());
}
}

运行后结果如下:我们发现实际的num值依然是100,而其他五个子线程中的num值则已经被修改了,这便证实了 ThreadLocal 变量,会让每个线程拥有其一个副本,在子线程中都是访问和操作这个副本。

1
2
3
4
5
6
Thread-0修改后的值为:20
Thread-4修改后的值为:24
Thread-1修改后的值为:21
Thread-2修改后的值为:22
Thread-3修改后的值为:23
num实际的值为:100

ThreadLocal的原理解析

在 Thread 类中有一个成员变量是 threadLocals

1
ThreadLocal.ThreadLocalMap threadLocals = null;

这个变量的作用就是用来保存其它线程中 ThreadLocal 变量的副本。因此每个线程中都会有一个 threadLocals,不过最开始都是 null,等待着你给它设置。下面给大家画了一张图,便于整体上对 ThreadLocal 一个总体的认识。

从上面这张图我们可以大概知道:

1.每个线程中都会有一个 ThreadLocalMap 变量。

2.ThreadLocalMap 中内部是一个个 Entry ,其中的 keyThreadLocal 本身,而 value 是创建 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
static class ThreadLocalMap {

//弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {

private static final int INITIAL_CAPACITY = 16;

Object value;

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

//Entry数组,用于多个 ThreadLocal<T> 的储存
private Entry[] table;

//创建ThreadLocalMap,ThreadLocal作为key,以ThreadLocal中的泛型为值
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//创建 Entry
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}

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
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//拿到当前线程的 ThreadLocal.ThreadLocalMap 变量 threadLocals
ThreadLocalMap map = getMap(t);
if (map != null)
//如果不为 threadLocals 不为空就调用 ThreadLocalMap 中的 set 方法
map.set(this, value);
else
//空的话就创建一个 ThreadLocalMap 变量,并把 value 保存进去
createMap(t, value);



-------------------------------------------------------------------------

//ThreadLocalMap 中的 set() 方法
//这里是真正给entry设置的地方
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;
//经过复杂位运算获得entry数组中的对应的下表
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;
}

//对 key 为 null 的entry清除,但是清除不是非常及时,会造成内存泄漏,因此当我们用完ThreadLocal时一定要记得 remove()
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

//创建entry并保存值
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
}

get() 方法源码

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
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//拿到当前线程的 ThreadLocal.ThreadLocalMap 变量 threadLocals
ThreadLocalMap map = getMap(t);
if (map != null) {
//如果不为 threadLocals 不为空就调用 getEntry() 方法
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//如果 e 不为 null,就返回entry的value
T result = (T)e.value;
return result;
}
}
//如果map是空,就调用setInitialValue()方法,实际上最终返回的是 null
return setInitialValue();
}



-------------------------------------------------------------------------

//getEntry() 方法
//真正得到 value 的方法
private Entry getEntry(ThreadLocal<?> key) {
//经过复杂位运算获得entry数组中的对应的下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
//entry 不为 null 且有相应的 key 就返回对应数组的元素值
return e;
else
//不满足上述条件的话就会调用 getEntryAfterMiss()
return getEntryAfterMiss(key, i, e);
}

getEntryAfterMiss()这个方法会判断 entry 是否为空,如果为空就会直接返回 null,否则将进一步判断 key 是否为空,若不为空就返回此 entry,如果为空那么就会做一波清理,清理掉过时的 entry

remove() 方法源码

1
2
3
4
5
6
//这个方法就是用来清除 threadLocals 的,防止内存泄漏
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

ThreadLocal 的 Entry 中的 Key 为什么要用弱引用?

1
2
3
4
5
6
7
8
9
10
//WeakReference --------------> 弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

在说这个问题之前我们先来看看 JDK 为我们提供了哪些引用:

  • 强引用 : 像 Objective o = new Object() 这种直接用 = 引用的就属于强引用,在每次 GC 时会进行 根可达分析,如果能够与 GCRoots 相关联,那么就永远不会被垃圾回收。
  • 软引用:一些有用但是并非必需。用软引用关联的对象,系统将要发生内存溢出(OuyOfMemory)之前,这些对象就会被回收,如果这次回收后还是没有足够的空间,才会抛出内存溢出。
  • 弱引用:一些有用(程度比软引用更低)但是并非必需。用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC 发生时,不管内存够不够,都会被回收。
  • 虚引用:最弱,它随时可能被回收掉。垃圾回收的时候收到一个通知,主要用来监控垃圾回收器是否正常工作。

照这么说的话弱引用能够在 GC 时被清除掉,那用在 Entry中的Key 中有什么用呢,用强引用难道就不行了吗?

诶,答案是还真不行。我们来看看 Entry 在堆里的结构:

如果 Entry 中的 key 是强引用,那么当 ThreadLocal 变量 tl 手动设置为 null 时,由于 Entry 中还有对 ThreadLocal 对象的强引用,因此它无法被回收。而我们栈中 tl 变量已经是 null,无法通过 key 来访问 Entry 中的变量了,但这个 Entry 由于持有一个强引用又不会被垃圾回收,因此这就造成了 内存泄漏

那我们来看看用 弱引用 是什么情况,当我们使用完 tl 变量后将其设置为 null 后,因为是 弱引用,因此 ThreadLocal 会被垃圾回收。当被回收之后, Entry 中的 key 就是 null 了,而之前我们讲到过在 get()set()remove()方法中都会对 keynullentry 进行清除处理,因此不会造成内存泄漏,这就是 JDK虚引用 的精妙之处。

因此建议大家在用完 ThreadLocal 变量之后一定记得要进行 tl.remove() 操作,用来避免内存泄漏这一问题。