ThreadLocal弱引用与内存泄漏分析

本文对ThreadLocal弱引用进行一些解析,以及ThreadLocal使用注意事项。java

ThreadLocal

首先,简单回顾一下,ThreadLocal是一个线程本地变量,每一个线程维护本身的变量副本,多个线程互相不可见,所以多线程操做该变量没必要加锁,适合不一样线程使用不一样变量值的场景。数组

其实现原理这里就不作详细阐述,其数据结构是每一个线程Thread类都有个属性ThreadLocalMap,用来维护该线程的多个ThreadLocal变量,该Map是自定义实现的Entry<K,V>[]数组结构,并不是继承自原生Map类,Entry其中Key便是ThreadLocal变量自己,Value则是具体该线程中的变量副本值。结构如图:数据结构

threadlocal.png

所以ThreadLocal其实只是个符号意义,自己不存储变量,仅仅是用来索引各个线程中的变量副本。多线程

值得注意的是,Entry的Key即ThreadLocal对象是采用弱引用引入的,如源代码:this

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

本文下面重点分析为什么使用弱引用,以及可能存在的问题。spa

首先看下弱引用。线程

弱引用

java语言中为对象的引用分为了四个级别,分别为 强引用 、软引用、弱引用、虚引用。代码规范

其他三种具体可自行查阅相关资料。code

弱引用具体指的是java.lang.ref.WeakReference<T>类。对象

对对象进行弱引用不会影响垃圾回收器回收该对象,即若是一个对象只有弱引用存在了,则下次GC将会回收掉该对象(无论当前内存空间足够与否)。

再来讲说内存泄漏,假如一个短生命周期的对象被一个长生命周期对象长期持有引用,将会致使该短生命周期对象使用完以后得不到释放,从而致使内存泄漏。

所以,弱引用的做用就体现出来了,可使用弱引用来引用短生命周期对象,这样不会对垃圾回收器回收它形成影响,从而防止内存泄漏。

ThreadLocal中的弱引用

1.为何ThreadLocalMap使用弱引用存储ThreadLocal?

假如使用强引用,当ThreadLocal再也不使用须要回收时,发现某个线程中ThreadLocalMap存在该ThreadLocal的强引用,没法回收,形成内存泄漏。

所以,使用弱引用能够防止长期存在的线程(一般使用了线程池)致使ThreadLocal没法回收形成内存泄漏。

2.那一般说的ThreadLocal内存泄漏是如何引发的呢?

咱们注意到Entry对象中,虽然Key(ThreadLocal)是经过弱引用引入的,可是value即变量值自己是经过强引用引入。

这就致使,假如不做任何处理,因为ThreadLocalMap和线程的生命周期是一致的,当线程资源长期不释放,即便ThreadLocal自己因为弱引用机制已经回收掉了,但value仍是驻留在线程的ThreadLocalMap的Entry中。即存在key为null,但value却有值的无效Entry。致使内存泄漏。

但实际上,ThreadLocal内部已经为咱们作了必定的防止内存泄漏的工做。

即以下方法:

/**
         * 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.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        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;
            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;
        }

上述方法的做用是擦除某个下标的Entry(置为null,能够回收),同时检测整个Entry[]表中对key为null的Entry一并擦除,从新调整索引。

该方法,在每次调用ThreadLocal的get、set、remove方法时都会执行,即ThreadLocal内部已经帮咱们作了对key为null的Entry的清理工做。

可是该工做是有触发条件的,须要调用相应方法,假如咱们使用完以后不作任何处理是不会触发的。

总结

  • (强制)在代码逻辑中使用完ThreadLocal,都要调用remove方法,及时清理。

目前咱们使用多线程都是经过线程池管理的,对于核心线程数以内的线程都是长期驻留池内的。显式调用remove,一方面是防止内存泄漏,最为重要的是,不及时清除有可能致使严重的业务逻辑问题,产生线上故障(使用了上次未清除的值)。

最佳实践:在ThreadLocal使用先后都调用remove清理,同时对异常状况也要在finally中清理。

  • (非规范)对ThreadLocal是否使用全局static修饰的讨论。

在某些代码规范中遇到过这样一条要求:“尽可能不要使用全局的ThreadLocal”。关于这点有两种解读。最初个人解读是,由于静态变量的生命周期和类的生命周期是一致的,而类的卸载时机能够说比较苛刻,这会致使静态ThreadLocal没法被垃圾回收,容易出现内存泄漏。另外一个解读,我咨询了编写该规范的对方解释是,若是流程中改变了变量值,下次复用该流程可能致使获取到非预期的值。

但实际上,这两个解读都是没必要要的,首先,静态ThreadLocal资源回收的问题,即便ThreadLocal自己没法回收,但线程中的Entry是能够经过remove清理掉的也就不会出现泄漏。第二种解读,屡次复用值改变的问题,其实在调用remove后也不会出现。

而若是ThreadLocal不加static,则每次其所在类实例化时,都会有重复ThreadLocal建立。这样即便线程在访问时不出现错误也有资源浪费。

所以,ThreadLocal通常加static修饰,同时要遵循第一条及时清理

我的博客:www.hellolvs.cn

相关文章
相关标签/搜索