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 ,其中的 key 是 ThreadLocal 本身,而 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()方法中都会对 key 为 null 的 entry 进行清除处理,因此不会造成内存泄漏,这就是 JDK 用 虚引用 的精妙之处。

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