Java 中的 ThreadLocal
Java juc 面试 About 6,486 words说明
这里的内存泄露指的是:Thread长时间运行,尤其是线程池中的线程,由于Thread中的threadLocals中的Entry数组包含了关联ThreadLocal的弱引用和ThreadLocal设置的值的元素Entry,在没有调用remove的情况下,发生了垃圾回收,ThreadLocal被回收,而ThreadLocal设置的值还保存在Entry中,整个Entry也一直不为null,线程一直持有该Entry引用,得不到回收,占用内存,造成内存泄漏。
内存泄漏问题
set后没有remove。- 同一代码块未
set方法和未重写initValue方法,直接调用get方法,没有进行remove。 remove后再get。
set 后没有 remove 情况
当threadLocal未被回收前,Thread中的成员变量threadLocals(就是ThreadLocalMap)的Entry数组中持有threadLocal引用和threadLocal设置的value(设置在6号索引位置的Entry)。

GC回收后,由于没有调用remove方法,Entry数组仍然持有threadLocal设置的Entry,虽然referent被回收置空了,但value仍在。

threadLocal2设置一个value时(设置在13号位索引的Entry),可以看到Entry数组中有两个不为null的元素(6号和13号)。

当threadLocal2被remove后,发现threadLocal2设置的整个13号Entry对象都被置空回收了,只留了原先threadLocal的6号Entry对象。

new Thread(() -> {
Thread thread = Thread.currentThread();
ThreadLocal<Object> threadLocal = new ThreadLocal<>();
try {
threadLocal.set(thread.getName());
} finally {
// 故意不 remove
}
threadLocal = null;
System.gc();
ThreadLocal<Object> threadLocal2 = new ThreadLocal<>();
try {
threadLocal2.set(thread.getName());
} finally {
threadLocal2.remove();
}
threadLocal2 = null;
System.gc();
},"AAA").start();
未 set 直接 get 后没有 remove 情况
在未调用set方法及重写initValue初始化方法时,get方法会初始化一个value为null的Entry并放入ThreadLocalMap的Entry数组,同样没有进行remove操作。

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
fixedThreadPool.execute(() -> {
Thread thread = Thread.currentThread();
ThreadLocal<Object> threadLocal = new ThreadLocal<>();
try {
threadLocal.set(thread.getName());
} finally {
// threadLocal.remove();
}
threadLocal = null;
System.gc();
ThreadLocal<Object> threadLocal2 = new ThreadLocal<>();
Object o = threadLocal2.get();
threadLocal2 = null;
System.gc();
});
remove 后再次 get 情况
remove后再次调用get也会出现内存泄漏问题。

哈希碰撞问题
放入ThreadLocal到Entry数组时,如果发现已经索引位置上已经有其他的元素了,则判断,如果是同一个key就替换value,如果key为null(key为null的情况为先remove后get)就替换老的Entry,都不是就将索引往后移一位,循环判断。
这次处理哈希碰撞是移动到数组下一位索引。HashMap处理哈希碰撞是使用数组加链表的方式,哈希值冲突了就放到链表末尾(Java8)。
set 回收未移除的 Entry
Java8中ThreadLocal的set方法会尽可能的判断Entry数组中的Entry对象的key是否为空,为空则移除。
源码分析
ThreadLocal的set方法实则是调用ThreadLocalMap中的set方法,然后调用cleanSomeSlots判断Entry数组中Entry的key是否null,等于null则调用expungeStaleEntry清除该无用的Entry(若Entry数组大小大于等于threshold就进行rehash)。
cleanSomeSlots方法中判断的次数是每次循环后n无符号右移1位,若找到一个threadLocal为null的,再将n的值赋值为len长度,继续循环直到n为0,因为n的值是2的x幂,故没有threadLocal为空的情况下只会循环x次就跳出循环了。
所以set方法,只是尽可能的去弥补没有remove带来的问题,但还是有很大可能删除不到。
主要源码
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public T get() {
Thread t = Thread.currentThread();
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();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
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;
private Entry[] table;
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
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
// ...
return i;
}
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) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
}
————        END        ————
Give me a Star, Thanks:)
https://github.com/fendoudebb/LiteNote扫描下方二维码关注公众号和小程序↓↓↓