Java多线程实战|ThreadLocal造成内存泄漏原理解析


「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

前言:

接上文Java实战指南|玩转接口验签-你和高手只差俩个自定义注解 我们介绍了接口验签的时候,共享线程资源所使用的线程局部变量ThreadLocal,我们在使用的时候强调了,在拦截器的postHandle方法里一定要写ThreadContextHolder.destroy();也就是ThreadLocal.remove(),否则的话可能会造成内存泄漏,今天我们来分析一下ThreadLocal的源码 来看一下为什么会造成内存泄漏;

ThreadLocal

介绍:

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 */
复制代码

以上是官方介绍,大致意思就是,ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值;

ThreadLocal可以解释成线程的局部变量,也就是说一个ThreadLocal的变量只有当前自身线程可以访问,别的线程都访问不了,因此,ThreadLocal就避免了线程竞争,我们也可以说他是线程安全的,ThreadLocal从根源上就避免了线程冲突的发生。

基本使用

创建一个userId变量,由于ThreadLocal是一个泛型类,这里指定了userId的类型为整数。

private ThreadLocal<Integer> userId = new ThreadLocal<>();
复制代码

设置/获取 ThreadLocal的值,使用的时候我们可以把它想象成一个Map

public static void setUserId(Integer uid) {
        userId.set(uid);
    }
public static Integer getUserId() {
        return userId.get();
    }
复制代码

withInitial()方法:初始化ThreadLocal的值(所有线程可见)

private ThreadLocal<Integer> userId = ThreadLocal.withInitial(() -> 1001);
复制代码

源码解析

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        //获得当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap,
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        //通过当前实例获取ThreadLocalMap的变量,
         ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
            //获取ThreadLocal变量的value
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //初始化ThreadLocalMap
        return setInitialValue();
    }
复制代码

可以看到,ThreadLocal变量就是保存在每个线程的map中的。这个map就是Thread对象中的threadLocals字段。ThreadLocal.ThreadLocalMap threadLocals = null;

我们看一下ThreadLocalMap.Entry的源码:

/**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
    static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
复制代码

由上边代码我们可以发现Entry的key是一个弱应用,如果这个变量不再被其他对象使用时,自动回收这个ThreadLocal对象,避免可能的内存泄露,但是,Entry中的value,依然是强引用,不会被回收掉;

ThreadLocal内存泄漏原理分析:

上边我们说了ThreadLocalMap中的key是弱引用,当不存在外部强引用的时候,就会自动被回收,但是Entry中的value依然是强引用,那什么时候value在什么时候才会被回收呢?

由上边我们查看的源码我们可以看一下 value的引用链条:value->Entry->ThreadLocalMap->Thread;
由此可见,只要线程不销毁,我们的value就不会被回收掉,当然我们大部分的情况下 线程都是用完就销毁的,什么情况下不会销毁线程呢?相信很多人都有答案了,那就是线程池的常驻核心线程他会活跃在整个系统的生命周期中;

remove():
 /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
复制代码

其实ThreadLocal在我们去get的时候如果你get的key没有找到的话 他会自动清理ThreadLocalMap的,但是我们通常的使用基本上都是访问那几个不变的key,比我们上一篇文章我们每次请求都是查的userId;
可以看一下下边的源码,找不到key则自动清理getEntryAfterMiss

/**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        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;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
复制代码

set()方法和remove()方法都会调用到expungeStaleEntry(),进行value的清理;但是当你的get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行,如果你没有机会调用set()和remove(),那么这个内存泄漏依然会发生;

所以,当你不需要这个ThreadLocal变量时,主动调用remove()


ok!本期内容到此结束,你学废了吗,希望可以对大家有帮助,有不对的地方希望大家可以提出来的,共同成长;

整洁成就卓越代码,细节之中只有天地

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享