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扫描下方二维码关注公众号和小程序↓↓↓