ThreadLocal 使用与内存泄漏

Demo

先来看一下 ThreadLocal 的使用

public class ThreadLocalOutOfMemoryTest {
    static class LocalVariable {
        //总共有5M
        private byte[] locla = new byte[1024 * 1024 * 5];
    }

    // (1)创建了一个核心线程数和最大线程数为 6 的线程池,这个保证了线程池里面随时都有 6 个线程在运行
    final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(6, 6, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());
    // (2)创建了一个 ThreadLocal 的变量,泛型参数为 LocalVariable,LocalVariable 内部是一个 Long 数组
    static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();

    public static void main(String[] args) throws InterruptedException {
        // (3)向线程池里面放入 50 个任务
        for (int i = 0; i < 50; ++i) {
            poolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    // (4) 往threadLocal变量设置值
                    LocalVariable localVariable = new LocalVariable();
                    // 会覆盖
                    ThreadLocalOutOfMemoryTest.localVariable.set(localVariable);
                    // (5) 手动清理ThreadLocal
                    System.out.println("thread name end:" + Thread.currentThread().getName() + ", value:"+ ThreadLocalOutOfMemoryTest.localVariable.get());
//                    ThreadLocalOutOfMemoryTest.localVariable.remove();

                }
            });

            Thread.sleep(1000);
        }

        // (6)是否让key失效,都不影响。只要持有的线程存在,都无法回收。
        //ThreadLocalOutOfMemoryTest.localVariable = null;
        System.out.println("pool execute over");
    }
}

相当于说 ThreadLocal 有点想去超市的时的储物柜 ,每个顾客都是一个线程, 每个现在这个储物柜都拥有自己的一份东西 .

ThreadLocal 的使用场景 (举例)

Dubbo框架中 , RpcContext 在各个线程中传递 , 就是使用到了 ThreadLocal

源码分析

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;
    }

    static class ThreadLocalMap {

        // 数组的类型是继承 WeakReference 的  
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            // 构造函数持有两个东西 ,一个ThreadLocal ,一个 Object 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         * 
         * 存放了一个数组
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0
    }
    ....

}



public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    ....
}

基本上上面我们就可以知道存放线程的容器就是 ThreadLocalMap 里面的数组, 由于 ThreadLocalMap 是静态类, 所以多个 Thread 共同持有这个对象.

ThreadLocalMap线性探测法解决hash冲突

下面分析部分来自 : https://blog.csdn.net/xiaoxiaodaxiake/article/details/107732928
ThreadLocalMap 使用数组来存放数据有点像 HashMap ,但是它和 HashMap 的碰撞处理策略有点不同, HashMap 发生碰撞了就使用链表来解决 ,而 ThreadLocalMap 采用的是线性探测法 , 可以看下面这张图 :

1297993-20210904222715676-1732670868.png

当发生碰撞了, 则往后一个位子放, 那下一个位子有值了怎么办, 继续往下找 ,一直到找到,可是到了最好还是找不到怎么办 ,就从头开始找 . 再看一下代码细节


public class ThreadLocal<T> {

    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }




    static class ThreadLocalMap {
        // ThreadLocalMap 构造方法 
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

        /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }



        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            // for 的第二个条件 ,将会找下一个位置,直到找到才退出这个循环
            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;
            //清理一些槽位,并且重新 hash , 重新放进去 
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

    }

这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,这个值和斐波那契散列有关(这是一种乘数散列法,只不过这个乘数比较特殊,是32位整型上限2^32-1乘以黄金分割比例0.618…的值2654435769,用有符号整型表示就是-1640531527,去掉符号后16进制表示为0x61c88647),其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突。

ThreadLocalMap使用闭散列:(开放地址法或者也叫线性探测法)解决哈希冲突,线性探测法的地址增量di = 1, 2, … 其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

通过源码可以直到 ThreadLocalMap 在 get 和 set 的过程中, 其实还会顺便清理一下槽位, 可这样会可以避免发生内存泄漏了吗?

ThreadLocal 与内存泄漏

img

我们回到刚才说到的 ThreadLocalMap 的内部类对象 Entry 继承了 WeakReference , 先看一下内存溢出和内存泄漏

  1. Memory overflow:内存溢出是没有足够的内存提供申请者使用。
  2. Memory leak:内存泄漏是指程序中已动态分配的内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃的严重后果。内存泄漏的堆积终将导致内存溢出。

1297993-20210904224253032-140938608.png

我们看回 WeakReference

public class WeakReference<T> extends Reference<T> {
	...
}

引用相关的知识得看这一篇文章 , java-引用
简单地说 WeakReference 继承自 Reference , Reference 有个变量 reference 和GC相关 ,

先来看一种情景

1297993-20210904224337731-1246957701.png

Entry 假如是强引用, 假如有个线程使用了 ThreadLocal Ref, 当前的线程也使用了, 就像有个顾客使用了储物柜 ,然后走了,东西放在储物柜没拿, 那么该储物柜的某个位置一直被占用着, 就会造成内存泄漏 .

假如Entry 假如是强引用

  1. 假设在业务代码中使用完ThreadLocal,ThreadLocal Ref被回收了。
  2. 由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向ThreadLocal实例,所以ThreadLocal就可以顺利被回收,此时Entry中的key=null。
  3. 在没有手动删除这个Entry及CurrentThread依然运行的前提下,始终有强引用链threadRef->currentThread->ThreadLocalMap->entry->value。value不会被回收,而这块value永远不会被访问到了,导致value内存泄漏。

上面的意思是说 key 没了 , value 还在 ,会造成内存泄露

也就是说即使使用了弱引用也不能达到杜绝内存泄漏, 但是是不是使用了 WeakReference 没用作用呢? 也不是 ,相比于强引用 , ThreadLocal 在 get 和 set 的时候会判断某个 Entry 是不是失去引用了,假如失去引用了 ,那么它会置 null ,这样就可以避免内存泄漏, 但是这也得建立在外部调用 get/set 方法 ,万一外部没用调用 ThreadLocal 的 get/set 方法 ,那有可能存在内存泄露的风险.

我们以一个例子来说明 :

public class ThreadLocalDemo {
    public static void main(String[] args) throws InterruptedException {
        firstStack();
        // System.gc();
        Thread.sleep(1000);
        Thread thread = Thread.currentThread();
        System.out.println(thread); // 在这里打断点,观察thread对象里的ThreadLocalMap数据

    }
    // 通过是否获取返回值观察A对象里的local对象是否被回收
    private static A firstStack(){
        A a = new A();
        System.out.println("value: "+ a.get());
        return a;
    }
    private static class A{
        private ThreadLocal<String> local = ThreadLocal.withInitial(() -> "in class A");

        public String get(){
            return local.get();
        }
        public void set(String str){
            local.set(str);
        }

    }
}

我们先对 System.gc(); 这一句注释掉 , 然后打下短点 , 得到下面的堆栈信息 , 看到 ThreadLocal 相关的信息 , 可以看到 , 此时 reference 还是有值的.

img

假如我们此时放开注释

img

当我们 GC 的时候 ,此时A对象给回收 , ThreadLoal 肯定也给回收了, 所以 reference 就是为 null 了, 可是 value 还在啊!!!

假如要防止内存泄漏应该如何做呢?

  1. 使用完ThreadLocal,调用其remove方法删除对应的Entry
  2. 使用完ThreadLocal,当前Thread也随之运行结束

所以假如某个线程使用完了 ThreadLocal , 并且后续不再使用则建议调用remove方法.

参考

posted @ 2021-09-04 23:31  float123  阅读(144)  评论(0编辑  收藏  举报