ThreadLocal简介

java.lang.ThreadLocal的JDK文档介绍如下:

image-20230819132045069

image-20230819132053823

个人理解就是:

  1. ThreadLocal为线程提供了局部变量,每个线程访问ThreadLocal实例获取到的值都是独立存在的变量副本。
  2. 当Thread线程消失后,线程持有的变量副本都会被垃圾回收(除非这个变量副本还在被其他对象所引用,即强引用)。

使用ThreadLocal

先上示例代码:

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
public class ThreadLocalDemo {

private static ThreadLocal<String> LOCAL_VARIABLE = new ThreadLocal<>();

public void setValue(String s) {
LOCAL_VARIABLE.set(s);
}

public void printWithRemove() {
System.out.println(Thread.currentThread().getName() + "变量值为:" + LOCAL_VARIABLE.get());
LOCAL_VARIABLE.remove();
}

public static void main(String[] args) throws InterruptedException {

new Thread(() -> {
ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
threadLocalDemo.setValue("a");
threadLocalDemo.printWithRemove();
System.out.println(Thread.currentThread().getName() + "变量值为:" + LOCAL_VARIABLE.get());
}, "线程A").start();

Thread.sleep(3000L);
System.out.println(Thread.currentThread().getName() + "变量值为:" + LOCAL_VARIABLE.get());

new Thread(() -> {
ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
threadLocalDemo.setValue("b");
threadLocalDemo.printWithRemove();
System.out.println(Thread.currentThread().getName() + "变量值为:" + LOCAL_VARIABLE.get());
}, "线程B").start();
}
}

输出结果如下:

image-20230819140625134

在程序中,新启动了两个线程A、B,并在每个线程中对ThreadLocal进行了setgetremove操作。

printWithRemove方法中,通过get()方法直接拿取当前线程ThreadLocal实例的变量副本,拿取到了之前setValue方法赋予的值。

在调用ThreadLocal的remove方法后,再次使用get获取,会发现value变为了null,表示当前线程下ThreadLocal实例的变量副本已被删除。

在main方法中,直接获取ThreadLocal实例的变量副本结果为null,是因为main方法所运行的线程与A、B线程不为同一线程,通过Thread.currentThread().getName()也可以证明。由此也说明了,同一个ThreadLocal实例对象,在不同线程之间管理的变量副本是独立的。

正确使用ThreadLocal

  1. 将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

  2. 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。可以使用try-finally形式。

ThreadLocal源码解析

首先看其类定义:

1
2
3
4
5
6
7
8
9
10
11
public class ThreadLocal<T> {
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
// ...
}
static class ThreadLocalMap {
// ...
static class Entry extends WeakReference<ThreadLocal<?>> {
// ...
}
}
}

可以发现它就是一个public修饰的普通类,特殊点在于它存放于java.lang包下。

它有两个静态内部类:SuppliedThreadLocalThreadLocalMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* An extension of ThreadLocal that obtains its initial value from
* the specified {@code Supplier}.
*/
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

private final Supplier<? extends T> supplier;

SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}

@Override
protected T initialValue() {
return supplier.get();
}
}

SuppliedThreadLocal的内部结构比较简单,通过注释也可以发现,它是一个支持扩展初始化值方法的类。

再看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
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
static class ThreadLocalMap {

private Entry[] table;

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

/**
* 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;
}
}
}

ThreadLocalMap类内部还有一个Entry类,通过命名约定和注释文档可以大致了解到,ThreadLocalMap是一个定制的散列映射对象,只被ThreadLocal所使用,内部对象使用Entry存储,Entry的key为ThreadLocal,value为变量副本。

观察Entry的类结构以及构造方法,发现它继承了WeakReference(弱引用),并在实例化时调用了super(k),也就是说被指定为弱引用的ThreadLocal在作为key时,可能会出现entry(null, value)的情况,ThreadLocalMap会将这类entry标记为过期类型,在下一次使用中,将其value置为null,便于JVM垃圾回收。value被置为null的逻辑在expungeStaleEntry方法中。

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
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;

// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

再回过头从ThreadLocal看set方法:

1
2
3
4
5
6
7
8
9
10
11
 public void set(T value) {
Thread t = Thread.currentThread();
// 从当前线程拿取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
// map为空时,生成一个map
createMap(t, value);
}
}

这里面有两个逻辑是关键,第一个从当前线程拿取ThreadLocalMap,代表着是线程存储的ThreadLocalMap,而第二个map为空时,生成一个map,代表是ThreadLocal为Thread生成的ThreadLocalMap对象实例。

1
2
3
4
5
6
7
// 以下是ThreadLocal的实例方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
1
2
3
4
5
6
7
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

// ...
}

通过上面的源码,也说明了,ThreadLocal.ThreadLocalMap是存储在Thread中的,而Thread里面的threadLocals变量是通过ThreadLocal控制的。


再看ThreadLocal的get方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public T get() {
Thread t = Thread.currentThread();
// 从当前线程拿取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 以当前ThreadLocal实例对象作为key,从Entru拿取value
ThreadLocalMap.Entry e = map.getEntry(this);
// value不为空时就返回,为空也是走 setInitialValue 方法
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 线程中的ThreadLocalMap为空时,设置初始值并返回
return setInitialValue();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 private T setInitialValue() {
// ThreadLocal获取线程变量副本的初始值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}

remove方法:

1
2
3
4
5
6
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}

ThreadLocalMap#remove方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 将Entry的key置为null
e.clear();
// 将Entry的value置为null
expungeStaleEntry(i);
return;
}
}
}

ThreadLocal的使用场景

ThreadLocal一般适用于如下场景:

  1. 每个线程需要有自己独立的实例
  2. 实例需要在多个方法中被共享,又不希望被多线程共享

业务场景有:

  1. 存储用户session信息

    从token解析出用户信息之后,可能会在多处都要使用到,但又不想重复从token解析。

  2. 数据跨层传递(controller、service、dao)

    为了避免显式传参的麻烦。

  3. 链路id

    例如日志链路,为了方便后期使用日志排查问题,一般会将一个api请求的日志都加上一个相同链路id。