Java 中的 ThreadLocal

Java juc 面试 About 6,486 words

说明

这里的内存泄露指的是:Thread长时间运行,尤其是线程池中的线程,由于Thread中的threadLocals中的Entry数组包含了关联ThreadLocal的弱引用和ThreadLocal设置的值的元素Entry,在没有调用remove的情况下,发生了垃圾回收,ThreadLocal被回收,而ThreadLocal设置的值还保存在Entry中,整个Entry也一直不为null,线程一直持有该Entry引用,得不到回收,占用内存,造成内存泄漏。

内存泄漏问题

  1. set后没有remove
  2. 同一代码块未set方法和未重写initValue方法,直接调用get方法,没有进行remove
  3. remove后再get

set 后没有 remove 情况

threadLocal未被回收前,Thread中的成员变量threadLocals(就是ThreadLocalMap)的Entry数组中持有threadLocal引用和threadLocal设置的value(设置在6号索引位置的Entry)。

gc before.png

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

gc after.png

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

ThreadLocal2 put.png

threadLocal2remove后,发现threadLocal2设置的整个13Entry对象都被置空回收了,只留了原先threadLocal6Entry对象。

ThreadLocal2 remove.png

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方法会初始化一个valuenullEntry并放入ThreadLocalMapEntry数组,同样没有进行remove操作。

get without set.png

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也会出现内存泄漏问题。

get after remove.png

哈希碰撞问题

放入ThreadLocalEntry数组时,如果发现已经索引位置上已经有其他的元素了,则判断,如果是同一个key就替换value,如果keynullkeynull的情况为先removeget)就替换老的Entry,都不是就将索引往后移一位,循环判断。

这次处理哈希碰撞是移动到数组下一位索引。HashMap处理哈希碰撞是使用数组加链表的方式,哈希值冲突了就放到链表末尾(Java8)。

set 回收未移除的 Entry

Java8ThreadLocalset方法会尽可能的判断Entry数组中的Entry对象的key是否为空,为空则移除。

源码分析

ThreadLocalset方法实则是调用ThreadLocalMap中的set方法,然后调用cleanSomeSlots判断Entry数组中Entrykey是否null,等于null则调用expungeStaleEntry清除该无用的Entry(若Entry数组大小大于等于threshold就进行rehash)。

cleanSomeSlots方法中判断的次数是每次循环后n无符号右移1位,若找到一个threadLocalnull的,再将n的值赋值为len长度,继续循环直到n0,因为n的值是2x幂,故没有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;
                }
            }
        }
    }
Views: 2,766 · Posted: 2021-04-14

————        END        ————

Give me a Star, Thanks:)

https://github.com/fendoudebb/LiteNote

扫描下方二维码关注公众号和小程序↓↓↓

扫描下方二维码关注公众号和小程序↓↓↓


Today On History
Browsing Refresh