ThreadLocal原理解析!

ThreadLocal 提供了线程本地变量的实例,它与普通变量的区别在于。

  • 每个使用该变量的线程都会初始化一个完全独立的实例副本。

使用场景

ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本。

  • 这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。

ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景,避免了传参。

  • 类似于全局变量的概念。

用户登录态上下文:

  • 通过TheadLocal封装用户公共的上下文信息,可以将身份鉴定、权限等一系列公用内容统一处理,服务层直接应用。

关键属性

1
2
3
4
5
6
7
8
9
// threadLocalHashCode 表示当前 ThreadLocal 的 hashCode,用于计算当前 ThreadLocal 在 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();
// 计算 ThreadLocal 的 hashCode 值(就是递增)
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// static + AtomicInteger 保证了在一台机器中每个 ThreadLocal 的 threadLocalHashCode 是唯一的
// 被 static 修饰非常关键,因为一个线程在处理业务的过程中,ThreadLocalMap 是会被 set 多个 ThreadLocal 的,多个 ThreadLocal 就依靠 threadLocalHashCode 进行区分
private static AtomicInteger nextHashCode = new AtomicInteger();

ThreadLocalMap

ThreadLocalMap 本身就是一个简单的 Map 结构。

KeyThreadLocalValueThreadLocal 保存的值,底层是数组的数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static class ThreadLocalMap {
// 数组中的每个节点值,WeakReference 是弱引用,当没有引用指向时,会直接被回收
static class Entry extends WeakReference<ThreadLocal<?>> {
// 当前 ThreadLocal 关联的值
Object value;
// WeakReference 的引用 referent 就是 ThreadLocal
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 数组的初始化大小
private static final int INITIAL_CAPACITY = 16;
// 存储 ThreadLocal 的数组
private Entry[] table;
// 扩容的阈值,默认是数组大小的三分之二
private int threshold;
}

ThreadLocal 是如何做到线程之间数据隔离的

主要因为是 ThreadLocalMap 是线程的属性。

ThreadLocals.ThreadLocalMapInheritableThreadLocals.ThreadLocalMap 分别是线程的属性。

  • 所以每个线程的 ThreadLocals 都是隔离独享的。

父线程在创建子线程的情况下,会拷贝 inheritableThreadLocals 的值,但不会拷贝 threadLocals 的值。

set 方法

1
2
3
4
5
6
7
8
9
10
11
// set 操作每个线程都是串行的,不会有线程安全的问题
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 当前 thradLocal 之前有设置值,直接设置,否则初始化
if (map != null)
map.set(this, value);
// 初始化ThreadLocalMap
else
createMap(t, value);
}
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
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算 key 在数组中的下标,其实就是 ThreadLocal 的 hashCode 和数组大小-1取余
int i = key.threadLocalHashCode & (len-1);

// 整体策略:查看 i 索引位置有没有值,有值的话,索引位置 + 1,直到找到没有值的位置
// 这种解决 hash 冲突的策略,也导致了其在 get 时查找策略有所不同,体现在 getEntryAfterMiss 中
for (Entry e = tab[i];
e != null;
// nextIndex 就是让在不超过数组长度的基础上,把数组的索引位置 + 1
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到内存地址一样的 ThreadLocal,直接替换
if (k == key) {
e.value = value;
return;
}
// 当前 key 是 null,说明 ThreadLocal 被清理了,直接替换掉
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 当前 i 位置是无值的,可以被当前 thradLocal 使用
tab[i] = new Entry(key, value);
int sz = ++size;
// 当数组大小大于等于扩容阈值(数组大小的三分之二)时,进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

通过递增的 AtomicInteger 作为 ThreadLocalhashCode 的。

计算数组索引位置的公式是:

  • hashCode 取模数组大小,由于 hashCode 不断自增。
  • 所以不同的 hashCode 大概率上会计算到同一个数组的索引位置(在实际项目中,ThreadLocal 都很少,基本上不会冲突)

通过 hashCode 计算的索引位置 i 处如果已经有值了,会从 i 开始,通过 +1 不断的往后寻找。

  • 直到找到索引位置为空的地方,把当前 ThreadLocal 作为 key放进去。

get 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public T get() {
// 因为 threadLocal 属于线程的属性,所以需要先把当前线程拿出来
Thread t = Thread.currentThread();
// 从线程中拿到 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从 map 中拿到 entry,由于 ThreadLocalMap 在 set 时的 hash 冲突的策略不同,导致拿的时候逻辑也不太一样
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果不为空,读取当前 ThreadLocal 中保存的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 否则给当前线程的 ThreadLocal 初始化,并返回初始值 null
return setInitialValue();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 自旋 i+1,直到找到为止
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 在大量使用不同 key 的 ThreadLocal 时,其实还蛮耗性能的
while (e != null) {
ThreadLocal<?> k = e.get();
// 内存地址一样,表示找到了
if (k == key)
return e;
// 删除没用的 key
if (k == null)
expungeStaleEntry(i);
// 继续使索引位置 + 1
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

扩容

ThreadLocalMap 中的 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
//扩容
private void resize() {
// 拿出旧的数组
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 新数组的大小为老数组的两倍
int newLen = oldLen * 2;
// 初始化新数组
Entry[] newTab = new Entry[newLen];
int count = 0;
// 老数组的值拷贝到新数组上
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
// 计算 ThreadLocal 在新数组中的位置
int h = k.threadLocalHashCode & (newLen - 1);
// 如果索引 h 的位置值不为空,往后+1,直到找到值为空的索引位置
while (newTab[h] != null)
h = nextIndex(h, newLen);
// 给新数组赋值
newTab[h] = e;
count++;
}
}
}
// 给新数组初始化下次扩容阈值,为数组长度的三分之二
setThreshold(newLen);
size = count;
table = newTab;
}

扩容后数组大小是原来数组的两倍。

扩容时是没有线程安全问题的,因为 ThreadLocalMap 是线程的一个属性。

一个线程同一时刻只能对 ThreadLocalMap 进行操作,因为同一个线程执行业务逻辑必然是串行的。

  • 那么操作 ThreadLocalMap 必然也是串行的。

内存泄漏

ThreadLocalMap的每个Entry都是一个对key的弱引用,同时每个Entry都包含了一个对value的强引用。

  • 正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了。

但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收。

  • 因为有以下的调用链:Thread ---> ThreadLocalMap ---> Entry(key为null) ---> value

因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM。

JDK已经考虑到了这个问题,所以在set,remove,rehash方法中会扫描key为null的Entry。

  • 并把对应的value设置为null,这样value对象就可以被回收。

如何避免内存泄露

调用remove方法,就会删除对应的Entry对象,可以避兔内存泄漏。

  • 所以使用完ThreadLocal之后,应该调用remove方法。
1
2
3
4
5
6
7
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("月伴飞鱼");
// 其它业务逻辑
} finally {
localName.remove();
}

空指针问题

ThreadLocal在进行get之前,必须先set,否则会报空指针异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ThreadLocalNPE {
ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
public void set() {
longThreadLocal.set(Thread.currentThread().getId());
}
//拆装箱问题
public Long get() {//long:NullPointerException
Long res = longThreadLocal.get();
return res;
}

public static void main(String[] args) {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
System.out.println(threadLocalNPE.get());//NullPointerException
}
}

如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象。

那么多个线程的 ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。

InheritableThreadLocal

InheritableThreadLocal解决父子线程变量传递的问题。

  • 如果我在后面改了父线程,子线程不会更新它的本地变量Map

这个ThreadLocalMap的局部变量,实际作用是在子线程创建的时候:

  • 父线程会把threadLocal拷贝到子线程中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class InheritableThreadLocalSolution {

// 使用 ThreadLocal 存储当前线程的用户名
private static final ThreadLocal<String> userNameThreadLocal = new ThreadLocal<>();
// 使用 InheritableThreadLocal 存储当前线程的用户名,以便子线程可以继承
private static final InheritableThreadLocal<String> inheritableUserNameThreadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {
// 设置父线程的 ThreadLocal 值
userNameThreadLocal.set("Parent Thread");
inheritableUserNameThreadLocal.set("Parent Thread (Inheritable)");

// 创建子线程并启动
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
// 尝试从子线程中获取父线程的 ThreadLocal 值(通常获取不到)
System.out.println("From child thread (ThreadLocal): " + userNameThreadLocal.get());
// 尝试从子线程中获取父线程的 InheritableThreadLocal 值
System.out.println("From child thread (InheritableThreadLocal): " + inheritableUserNameThreadLocal.get());
});
executorService.shutdown();
}
}

InheritableThreadLocal的内存泄漏

当移除父线程的threadlocal,子线程的threadlocal并不会消失。

  • 并且通常来讲子线程是放线程池管理的,不会随着父线程的消失而消失。

所以子线程的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
34
public class InheritableThreadLocalSolution {

private static final InheritableThreadLocal<String> inheritableUserNameThreadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) throws InterruptedException {
inheritableUserNameThreadLocal.set("Parent Thread (Inheritable)");

ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("From child thread (InheritableThreadLocal): " + inheritableUserNameThreadLocal.get());
});

TimeUnit.SECONDS.sleep(5);
inheritableUserNameThreadLocal.remove();
// 提交第二个任务,看看子线程是否还会获得 inheritableThreadLocal的值
// 结果:会的
executorService.submit(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 父线程以及移除了,但是子线程仍然能获取到
System.out.println("second From child thread (InheritableThreadLocal): " + inheritableUserNameThreadLocal.get());
});
TimeUnit.SECONDS.sleep(10);
executorService.shutdown();
}
}

TransmittableThreadLocal

项目地址:https://gitee.com/mirrors/transmittable-thread-local

对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的。

  • 这时父子线程关系的ThreadLocal值传递已经没有意义
    • 需要把 任务提交给线程池时ThreadLocal值传递到 任务执行时

TransmittableThreadLocal解决线程池变量丢失问题。

  • 线程池会复用之前的线程,导致父线程的本地变量更新之后,之前创建的子线程拿不到这个值。

get/set 方法中完成了TransmittableThreadLocal的注册

  • 然后在执行run方法的时候通过TtlRunnable进行了方法包装

在调用之前进行快照形成,并应用快照到当前线程中

  • 最后在线程执行结束之后,run方法内部对线程局部变量做的修改则会被还原。

  • 通过将线程封装成TtlRunnable,然后通过快照还有hold一个总收集变量来解决。