ThreadLocal相关问题记录

doMore 1,405 2020-08-19

一、使用 ThreadLocal 需要注意什么问题

ThreadLocal 操作不当会引发内存泄漏。
原因:内部类 ThradLocalMap 中 Entry 的设计。Entry 继承了 WeakReference<ThreadLocal<?>>,即 Entry 的 key 是弱引用,所以 key 会在垃圾回收的时候被回收掉, 而 key 对应的 value 则不会被回收, 这样会导致一种现象: key 为 null,value 有值。
导致结果:value 会一直累积,最终内存泄漏,GC 无法回收。

// java 8 中 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;
            }
        }

二、如何解决内存泄漏问题

每次使用 ThreadLocal 之后手动调用 remove() 方法清除数据。remove() 方法会将当前的 key 和 value(Entry) 清除。

        /**
         * Remove the entry for key.
         */
        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) {
                    // 清除 key
                    e.clear();
                    // 清除 value
                    /*
                    * Expunge a stale entry by rehashing any possibly colliding entries
         			* lying between staleSlot and the next null slot.  This also expunges
         			* any other stale entries encountered before the trailing null.
         			*/
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

三、JDK开发者是如何避免内存泄漏的

ThreadLocal的设计者也意识到了这一点(内存泄漏), 他们在一些方法中埋了对key=null的value擦除操作。

这里拿ThreadLocal提供的get()方法举例,它调用了ThreadLocalMap#getEntry()方法,对key进行了校验和对null key进行擦除。

       /**
         * 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.
         */
        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);
        }
// else 中方法依次调用,最终擦除 value 值
// java.lang.ThreadLocal.ThreadLocalMap#getEntryAfterMiss
// java.lang.ThreadLocal.ThreadLocalMap#expungeStaleEntry

expungeStaleEntry 方法解释

expungeStaleEntry(i) 方法完成了对 key=null 的 key 所对应的 value 进行赋空, 释放了空间避免内存泄漏。

同时它遍历下一个key为空的entry, 并将value赋值为null, 等待下次GC释放掉其空间。

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
            Entry e;
            int i;
            // 遍历下一个 key 为 null 的 Entry,并将 value 指向 null 
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

同理,其他避免内存泄露的方式也是如此,最终都会调用到 expungeStaleEntry。

也只能说尽可能避免内存泄漏, 但并不会完全解决内存泄漏这个问题。比如极端情况下我们只创建ThreadLocal但不调用set、get、remove方法等。所以最能解决问题的办法就是用完ThreadLocal后手动调用remove().

四、手动释放ThreadLocal遗留存储?怎么去设计/实现?

手动释放,其实就是手动调用 remove() 。

我们可以借助 aop ,根据 bean 方法的执行方式,在其执行完成之后调用。

问题思考1:弱引用导致内存泄漏,那为什么key不设置为强引用?

如果 key 设置为强引用,当 threadLocal 实例释放之后,threadLocal = null, 但是 threadLocal 会有强引用指向 ThreadLocalMap,ThreadLocalMap.Entry 又有强引用指向 ThredLocal,这样会导致 threadLocal 不能正常被 GC 回收。

弱引用虽然会引起内存泄漏, 但是也有set、get、remove方法操作对null key进行擦除的补救措施, 方案上略胜一筹。

问题思考2:线程执行结束后会不会自动清空Entry的value?

当currentThread执行结束后, threadLocalMap变得不可达从而被回收,Entry等也就都被回收了,但这个环境就要求不对Thread进行复用,但是项目中经常会复用线程来提高性能, 所以currentThread一般不会处于终止状态。

五、Thread和ThreadLocal有什么联系?

Thread和ThreadLocal是绑定的, ThreadLocal依赖于Thread去执行, Thread将需要隔离的数据存放到ThreadLocal(准确的讲是ThreadLocalMap)中, 来实现多线程处理。

六、Spring如何处理Bean多线程下的并发问题

在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域,因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理,解决线程安全问题。

ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。

ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。