今天在重新看阿里Java手册的时候,看到了ThreadLocal,就想对ThreadLocal进一步了解下。
在讲ThreadLocal之前,先去了解了下SimpleDateFormat为什么不是线程安全的。先来看下SimpleDateFormat的部分源码,这个在网上应该也有讲解。
可以看这个 **原因**,讲解的挺详细的。
ThreadLocal
ThreadLocal为变量在每个线程中都创建了一个副本,所以每个线程可以访问自己内部的副本变量,不同线程之间不会互相干扰。Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量。
实现原理
每一个线程持有一个ThreadLocalMap。
一个Thread中只有一个 ThreadLocalMap,一个 ThreadLocalMap 中可以有多个 ThreadLocal 对象,其中一个 ThreadLocal 对象对应一个ThreadLocalMap中的一个Entry(也就是说:一个Thread可以依附有多个ThreadLocal对象)。
ThreadLocalMap 和 WeakReference
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,会进行扩容操作
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
方法中有回收的代码:
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
。