0%

多线程编程 —— ThreadLocal

如果本文有错误,希望指出。

今天在重新看阿里Java手册的时候,看到了ThreadLocal,就想对ThreadLocal进一步了解下。

在讲ThreadLocal之前,先去了解了下SimpleDateFormat为什么不是线程安全的。先来看下SimpleDateFormat的部分源码,这个在网上应该也有讲解。

可以看这个 原因,讲解的挺详细的。

ThreadLocal

ThreadLocal为变量在每个线程中都创建了一个副本,所以每个线程可以访问自己内部的副本变量,不同线程之间不会互相干扰。Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量。

实现原理

每一个线程持有一个ThreadLocalMap。

一个Thread中只有一个 ThreadLocalMap,一个 ThreadLocalMap 中可以有多个 ThreadLocal 对象,其中一个 ThreadLocal 对象对应一个ThreadLocalMap中的一个Entry(也就是说:一个Thread可以依附有多个ThreadLocal对象)。

ThreadLocalMap 和 WeakReference

1
2
3
4
5
6
7
8
9
10
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;//默认初始化16容量
}

ThreadLocalMap 从字面上可以看出是保存 ThreadLocal 对象的 map(其实是以 ThreadLocal 为 key),不过是经过两层包装:

  • 第一次使用 WeakReference<ThreadLocal<?>> 将 ThreadLocal 对象变成一个弱引用对象。
  • 第二层是定义一个专门的类 Entry 来扩展WeakReference<ThreadLocal<?>>

类 Entry 保存 map 键值对的实体,ThreadLocal<?> 为 key,保存的线程局部变量值为 value。super(k) 调用的是 WeakReference 的构造函数,表示将 ThreadLocal<?> 变为弱引用。

TreadLocal 构造函数

TreadLocal 的 set 方法

table扩容

如果table中的元素数量达到阈值threshold的3/4,会进行扩容操作

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
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2; //旧的大小的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) { //如果key为null,回收value
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}

setThreshold(newLen);
size = count;
table = newTab;
}

ThreadLocal 内存回收

ThreadLocal 涉及到两个层面的内存回收。

ThreadLocal 层面的内存回收

当线程死亡,所有的保存的线程局部变量就会被回收,其实这里只线程 Thread 对象中的 ThreadLocal.ThreadLocalMap threadLocals 会被回收。

ThreadLocalMap 层面的内存回收

当线程存活的时间够长,并且该线程保存的线程局部变量很多,就需要在线程的生命期内进行 ThreadLocalMap 的内存回收。
Entry 对象的key 是WeakReference 的包装,当 ThreadLocalMap 的 private Entry[] table,已经被占用达到 2/3 (线程拥有的局部变量超过10个)时,就会尝试回收。在 ThreadLocalMap.set 方法中有回收的代码:

1
2
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();

cleanSomeSlots 具体回收代码:

ThreadLocal 可能引起的 OOM 问题

在一个线程结束的时候,Thread 会调用 exit 方法进行回收。

但是,当我们使用线程池的时候,这意味着当前线程未必会退出。这可能使得一些大的对象设置到 ThreadLocal 中,导致出现 OOM。比如当线程是设置固定值,第一次处理业务时,向 ThreadLocalMap 中存放了一个很大的对象,第二次,第三次。。。,线程一直在运行,这会导致这个线程的出现 OOM。

ThreadLocalMap的key为弱引用

关于弱引用、强引用这些,可以看深入理解虚拟机——垃圾收集器,这里面稍微讲解了下这方面。
ThreadLocalMap 会在下一次GC的时候,回收掉 key,而ThreadLocal 在下一次调用 get、set 和 remove,会清除并重构 ThreadLocalMap,其方法是 expungeStaleEntry

Reference

客官,赏一杯coffee嘛~~~~