前言
ThreadLocal 用于解决多线程对于共享变量的访问带来的安全性问题。ThreadLocal 存储线程局部变量。每个线程内置 ThreadLocalMap,ThreadLocalMap 的 key 存储 ThreadLocal 实例,value 存储自定义的值。与同步机制相比,它是一种“空间换时间”的方式,可以做到“访问并行化,对象专享化”。缺点是如果使用不当,则容易发生内存泄漏。
内存泄漏:指程序中已动态分配的堆内存由于某种原因未释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
ThreadLocal 的生命周期与线程相同。如果线程一直存活,并且 ThreadLocal 实例可以被访问,那么线程会一直持有 ThreadLocal 实例的副本的弱引用。一旦线程的生命周期结束,所有与 ThreadLocal 实例相关的副本都会被回收,除非有其它地方一直引用这些副本。
为什么是弱引用
使用强引用、弱引用两种引用方式作为对比。
强引用:一直存活,除非 GC Roots 不可达。只要强引用指向一个对象,则该对象表示存活,也就不会被垃圾回收。
弱引用:只能存活到下一次垃圾回收前。垃圾收集器一旦发现某个对象只具有弱引用,就会将该对象回收。
假设 Entry 的 key 使用强引用,如果没有手动删除这个 Entry,并且线程还在运行。在业务代码中使用完 ThreadLocal,然后 ThreadLocalRef 被回收。此时依然有强引用链 CurrentThreadRef -> CurrentThread -> ThreadLocalMap -> Entry,因此 Entry 不会被回收。并且 Entry 的 key 一直持有对 ThreadLocal 实例的强引用,导致 ThreadLocal 实例不会被回收,从而导致 ThreadLocal 的内存泄漏。
假设 Entry 的 key 使用弱引用,如果没有手动删除这个 Entry,并且线程还在运行。在业务代码中使用完 ThreadLocal,然后 ThreadLocalRef 被回收。由于 Entry 的 key 只持有 ThreadLocal 实例的弱引用,没有任何强引用指向 ThreadLocal 实例,所以 ThreadLocal 实例可以顺利被回收。此时 Entry 的 key 为 null,但是依然有强引用链 CurrentThreadRef -> CurrentThread -> ThreadLocalMap -> Entry -> value,因此 value 不会被回收。此外无法通过为 null 的 key 找到对应的 value,也就是说 value 无法被访问到,最终导致 value 内存泄漏。
上述两种情况都会有内存泄漏的问题。但是它们都有两个前提:没有手动删除这个 Entry、当前线程一直在运行。
因此,解决内存泄漏也可以针对这两个前提。
- 只要使用完 ThreadLocal,就调用 remove 方法删除对应的 Entry。
- 使用完 ThreadLocal,就让对应的线程随之结束。
相较于强引用,弱引用发生内存泄漏的实质上是无法根据为 null 的 key 找到对应的 value。而在 ThreadLocalMap 中的 set、getEntry、remove 方法会对 key 是否为 null 进行判断,如果为 null, 就会对对应的 value 设置为 null。这样就算忘记调用 ThreadLocal 的 remove 方法,弱引用也会比强引用多了一层保障。ThreadLocal 的 set、get、remove 方法底层也会分别调用 ThreadLocalMap 的 set、getEntry。也就是说,ThreadLocalMap 的 Entry 的 key 为 null 时,其 value 可以在下一次调用 ThreadLocal 的 get、set、remove 任一方法时都会被清除,从而避免了内存泄漏问题。
解决哈希冲突
ThreadLocalMap 使用闭散列(或者开放地址法、线性探测法)解决哈希冲突。具体的每次探测下一个地址,直到有空的地址可以插入,如果整个空间都没有空余的地址可以插入,就会产生内存溢出。
假设当前 table 长度为 16,也就是说如果计算出来 key 的 hash 值为 14,如果 table[14] 上已经有值,并且 key 与当前 key 不一致,那么就发生了哈希冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,这个时候如果还是冲突会回到 0,取 table[0],以此类推直到可以插入。
内存溢出:系统中没有足够的内存提供给申请者使用。
使用
在《Java 开发手册-嵩山版》中,关于 ThreadLocal 的注意事项如下:
必须回收自定义的 ThreadLocal 变量,尤其是在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄漏等问题。尽量使用 try-finally 块中进行回收。
e.g.
public class ThreadLocalDemo {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
public static void main(String[] args) {
// 输出1
System.out.println(threadLocal.get());
threadLocal.set(threadLocal.get() + 1);
// 输出2
System.out.println(threadLocal.get());
threadLocal.remove();
// 输出1(删除之后,得到的是初始值)
System.out.println(threadLocal.get());
}
}
public class ThreadLocalDemo2 implements Runnable {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
private Integer getNextNum() {
threadLocal.set(threadLocal.get() + 1);
return threadLocal.get();
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ":" + getNextNum());
}
}
public static void main(String[] args) {
ThreadLocalDemo instance = new ThreadLocalDemo();
for (int i = 0; i < 3; i++) {
// 每个线程依次输出1、2、3
new Thread(instance).start();
}
// 输出0
System.out.println(threadLocal.get());
}
}
public class ThreadLocalDemo3 {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
threadLocal.remove();
}
};
}
}
源码分析
先看下 ThreadLocal 的 set 方法,如下:
public void set(T value) {
Thread t = Thread.currentThread();
// 获取线程的ThreadLocalMap属性
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
第一次调用会走 createMap 方法。
void createMap(Thread t, T firstValue) {
// 实例化 ThreadLocalMap,其中 key 为 ThreadLocal 实例,value 为自定义的值
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
接下来看下 ThreadLocalMap 的 set 方法,如下:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算 key 的下标位置
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
// i:(i + 1 < len) ? i + 1 : 0
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
// 如果 key 为 null,则调用 replaceStaleEntry 方法将 value 设置为 null
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 将 key、value 插入到 Entry 中
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
然后看下 replaceStaleEntry 方法的处理,如下:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 将 value 设置为 null
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
接下来看下 ThreadLocal 的 get 方法,如下:
public T get() {
Thread t = Thread.currentThread();
// 获取线程的ThreadLocalMap属性
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
然后看下 setInitialValue 方法的处理,如下:
private T setInitialValue() {
// 获取初始值,默认 null
T value = initialValue();
Thread t = Thread.currentThread();
// 获取线程的ThreadLocalMap属性
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 实例化 ThreadLocalMap,其中 key 为 ThreadLocal 实例,value 为自定义的值
createMap(t, value);
return value;
}
接下来看下 ThreadLocalMap 的 getEntry 方法,如下:
private Entry getEntry(ThreadLocal<?> key) {
// 计算 key 对应的下标位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 如果命中,则返回对应的 Entry
if (e != null && e.get() == key)
return e;
// 如果没有命中,则调用 getEntryAfterMiss 方法
else
return getEntryAfterMiss(key, i, e);
}
然后看下 getEntryAfterMiss 方法,如下:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
// 如果 key 为 null,则调用 expungeStaleEntry 方法,将 value 设置为 null
if (k == null)
expungeStaleEntry(i);
else
// i:(i + 1 < len) ? i + 1 : 0
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
看下 expungeStaleEntry 方法:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 将指定下标位置的 Entry 的 value 设置为 null
tab[staleSlot].value = null;
// 将指定下标位置的 Entry 设置为 null
tab[staleSlot] = null;
size--;
Entry e;
int i;
// 从指定下标位置开始遍历
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果 key 为 null,则将 value 设置为 null
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 重新计算 key 的下标位置
int h = k.threadLocalHashCode & (len - 1);
// 如果下标位置发生变化
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
// 将 Entry 迁移到新的位置
tab[h] = e;
}
}
}
return i;
}
最后看下 ThreadLocal 的 remove 方法,如下:
public void remove() {
// 获取线程的ThreadLocalMap属性
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
接着看下 ThreadLocalMap 的 remove 方法,如下:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 计算 ThreadLocal 实例在 ThreadLocalMap 的下标位置
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 清除 Entry
e.clear();
// 调用 expungeStaleEntry 方法
// 如果 key 为 null,则将 value 设置为 null
expungeStaleEntry(i);
return;
}
}
}