ThreadLocal进阶之源码详解一波

这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

前言

  • ThreadLocal的使用场景在实际项目中是比较多的一个类,其主要能解决数据在线程间共享问题。也是后端开发中比较基础且必须掌握的一个知识点。
  • 线程间数据共享与不共享需要根据实际场景。
    • 比如场景要求线程间交替打印某某或累计计数等,就需要共享数据;
    • 比如传入线程内的用户信息等,就要避免线程共享;
  • 网上其实也有非常多关于ThreadLocal的文章,这篇文章结合自己对其的理解把大致原理输出一下。

示例

  • ThreadLocal的用法比较简单,下面是一个简单的示例demo。
  • 在demo中创建了三个线程,每个线程都是对同一个ThreadLocal对象进行操作,但最后输出打印结果却是各自的线程名称,说明其具有线程隔离效果。
/**
 * @Author: ZRH
 */
@Slf4j
public class ThreadLocalDemo {

    private static ThreadLocal<String> local = ThreadLocal.withInitial(() -> Thread.currentThread().getName());
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>(5));

    public static void main (String[] args) {
        for (int i = 0; i <= 2; i++) {
            executor.execute(() -> {
                log.info("本次打印的线程名称:{}", local.get());
            });
        }
    }
}
--------------------------------------打印结果
10:24:10.357 [pool-1-thread-2] INFO com.redisson.web.demo.ThreadLocalDemo - 本次打印的线程名称:pool-1-thread-2
10:24:10.356 [pool-1-thread-3] INFO com.redisson.web.demo.ThreadLocalDemo - 本次打印的线程名称:pool-1-thread-3
10:24:10.357 [pool-1-thread-1] INFO com.redisson.web.demo.ThreadLocalDemo - 本次打印的线程名称:pool-1-thread-1
复制代码

源码解析

  • ThreadLocal线程隔离是通过为每个线程都复制一份变量数据,所以每个线程都是操作自己本地的变量。
  • ThreadLocal可以通过普通的构造函数进行创建实例,也可以通过静态方法withInitial(...)创建实例。
  • SuppliedThreadLocal是ThreadLocal的一个扩展的内部类,用于设定指定的初始值。
    /**
     * Creates a thread local variable. The initial value of the variable is
     * determined by invoking the {@code get} method on the {@code Supplier}.
     *
     * @param <S> the type of the thread local's value
     * @param supplier the supplier to be used to determine the initial value
     * @return a new thread local variable
     * @throws NullPointerException if the specified supplier is null
     * @since 1.8
     */
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

    /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }
复制代码
  • 在Thread类中有ThreadLocal成员属性变量,初始值为null。
  • ThreadLocalMap是ThreadLocal的内部类,里面有个entry[]数组,在其构造函数中传入的k最后会被WeakReference所引用,所以这里以ThreadLocal实例为key才是弱引用,value值是强引用。
  • 在ThreadLocalMap构造函数中,会先创建一个大小为16的entry[]数组,然后通过firstKey计算出一个hash下标,并把value所在的entry对象加入数组,并设置下次扩容限制大小。
    static class ThreadLocalMap {

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

        /**
         * 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;

        ......
        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        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);
        }
    ......
    }
复制代码
  • 通过源码了解是通过ThreadLocal.set(...)方法向ThreadLocalMap中添加元素,其执行流程:
    • 先获取当前线程对象Thread,然后通过当前线程获取ThreadLocalMap集合
    • 如果ThreadLocalMap不为null,就通过ThreadLocal实例为key和value添加到ThreadLocalMap集合中
    • 如果ThreadLocalMap为null,就new ThreadLocalMap()对象并以ThreadLocal实例为key和value加入到集合中
    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    ......
    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
复制代码
  • 在上述的ThreadLocalMap对象结构和HashMap有的类似,但是在其set方法中有区别,最主要的是其发生hash冲突后是通过开放寻址法来解决,而HashMap是通过链表+红黑树解决。
    • 首先计算key对应的数组下标,
    • 然后判断数组内的key和当前插入的key是否相同,
    • 如果相等就替换value,然后返回。
    • 如果不相等,就调用replaceStaleEntry(...)方法清除数组内无用key的entry元素,
    • 然后执行nextIndex(...)方法得到下标+1位置的数组元素,再次进入循环操作,
    • 最后如果都没有匹配到,就清除一些无用Entry解决,然后在判断是否需要再次进行扩容。
    /**
     * Set the value associated with key.
     *
     * @param key the thread local object
     * @param value the value to be set
     */
    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 (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();
    }
复制代码
  • 元素插入ThreadLocal后肯定是还要获取的,通过ThreadLocal.get(...)方法向ThreadLocalMap中获取元素,其执行流程:
    • 先获取当前线程Thread对象,然后在获取ThreadLocalMap对象,
    • 然后通过getEntry(...)方法,以当前ThreadLocal实例为参数获取Entry元素,
    • 如果获取Entry成功,就直接返回,否则执行setInitialValue()方法。
    ......
    /**
     * 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 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();
    }
复制代码
  • 在ThreadLocalMap中getEntry(...)方法中:
    • 首先根据传进来的ThreadLocal实例参数为key,并计算出一个数组下标,
    • 根据下标获取数组内Entry元素,如果元素和本次key相同就直接返回。
    • 否则进入getEntryAfterMiss(...)方法继续查找:
    • 先判断传入的entry是否是本次key相同,
    • 如果不相同就执行expungeStaleEntry(...)方法,用于清除数组内key为null的entry元素,
    • 然后执行nextIndex(...)方法得到数组中下标+1的元素,再次遍历循环
    • 最后如果还是没有找到,就返回null。
    ......
    /**
     * 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;
    }
复制代码

内存泄漏原因

  • 在上述的分析中,可以发现ThreadLocalMap中entry对象内的key(ThreadLocal实例)是弱引用,那就其存活时机不会超过一个GC。
  • 所以当外部没有对当前ThreadLocal实例有强引用时,那就让其被JVM系统回收清理掉。
  • 为啥这样设计,网上解释也有很多,读者可以自行了解,这里按下不表。
  • 在实际项目中,一般会使用线程池方式创建线程执行任务,例如Tomact线程池。当弱引用的key被回收掉了后,其实对应entry也应该被回收掉,但因为线程复用的原因,使Thread->ThreadLocalMap->Entry链路继续存在,导致无用的entry一直回收不了,所以会发生内存泄漏。
  • 其实在ThreadLocal源码中有很多针对entry回收的处理,比如ThreadLocalMap.remove(...),ThreadLocalMap.getEntryAfterMiss(...)和ThreadLocalMap.set(...)等方法中都有清除无用entry操作。
  • 但上述还是无法避免可能会出现的内存泄漏,所以这里提一下ThreadLocal使用会造成内存泄漏的场景:
    • 在实际项目中,使用线程池方式执行任务,就会有线程的复用情况。
    • 在使用ThreadLocal对象时,没有手动remove(),也没有重新get()或者set()元素或触发扩容操作。

最后

  • ThreadLocal除了上述会出现内存泄漏外,还有其它不足之处。就是在当前线程中创建子线程时无法把ThreadLocal中变量传递下去。
  • 进阶:如果想实现父子线程传递变量,可以使用InheritableThreadLocal对象传递。
  • 虚心学习,共同进步 -_-

猜你喜欢

转载自juejin.im/post/7054858259995459621