ThreadLocal小结-到底会不会引起内存泄露

[TOC]

1. ThreadLocal简介

网上看到一些文章,提到关于ThreadLocal可能引起的内存泄露,搞得都不敢在代码里随意使用了,于是来研究下,看看到底ThreadLocal会不会导致内存泄露,什么情况下会导致泄露。

ThreadLocal,顾名思义,其存储的内容是线程私本地的/私有的,我们常使用ThreadLocal来存储/维护一些资源或者变量,以避免线程争用或者同步问题,例如使用ThreadLocal来为每个线程维持一个redis连接(生产中这也许不是一个好的方式,还是推荐专业的连接池)或者维持一些线程私有的变量等。

例如,假设我们在一个线程应用中需要对时间做格式化,我们很容易想到的是使用SimpleDateFormat这个工具类,但是SimpleDateFormat不是线程安全的,那么我们通常用两种做法:

  • 每次用到的时候new一个SimpleDateFormat对象,使用完丢弃,交给gc
  • 每个线程维护一个SimpleDateFormat实例,线程运行期间不重复创建

那么无论从执行效率还是内存占用方面,我们都倾向于使用后者,即线程私有一个SimpleDateFormat对象,这时候,ThreadLocal就是很好的应用,示例代码如下:

import java.text.SimpleDateFormat;
import java.util.Date;

public class TestTask implements Runnable {
    private boolean stop = false;
    private ThreadLocal<SimpleDateFormat> sdfHolder = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyyMMdd");
        }
    };

    @Override
    public void run() {
        while (!stop) {
            String formatedDateStr = sdfHolder.get().format(new Date());
            System.out.println("formated date str:" + formatedDateStr);
        //may be sleep for a while to avoid high cpu cost
        }
        sdfHolder.remove();
    }
    
    //something else
}

代码中模拟了一个需要反复执行的Task,其run方法中,while条件除非stop是true,否则就一直运转下去。在该示例中通过ThreadLocal为每个线程实例化了一个SimpleDateFormat对象,当需要的时候,通过get()获取即可,实现了每个线程全程只有一个SimpleDateFormat对象。同时在stop为true时使用ThreadLocal的remove方法删除当前线程使用的SimpleDateFormat对象,以便于垃圾回收。

仅演示ThreadLocal用法,暂不讨论代码设计

2. ThreadLocal内存模型

上面我们简单介绍了ThreadLocal的概念和使用,下面看下ThreadLocal的内存模型。

2.1 ThreadLocal内存模型

2.1.1 私有变量存储在哪里

在代码中,我们使用ThreadLocal实例提供的set/get方法来存储/使用value,但ThreadLocal实例其实只是一个引用,真正存储值的是一个Map,其key实ThreadLocal实例本身,value是我们设置的值,分布在堆区。这个Map的类型是ThreadL.ThreadLocalMap(ThreadLocalMap是ThreadLocal的内部类),其key的类型是ThreadLocal,value是Object,类定义如下:

    static class 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);
        }
        static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
    }

那么当我们重写init或者调用set/get的时候,内部的逻辑是怎样的呢,按照上面的说法,应该是将value存储到了ThreadLocalMap中,或者从已有的ThreadLocalMap中获取value,我们来通过代码分析一下。

ThreadLocal.set(T value)

set的逻辑比较简单,就是获取当前线程的ThreadLocalMap,然后往map里添加KV,K是this,也就是当前ThreadLocal实例,V是我们传入的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);
    }

其内部实现首先需要获取关联的Map,我们看下getMap和createMap的实现

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    /**
     * 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
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

可以看到,getMap就是返回了当前Thread实例的map(t.threadLocals),create也是创建了Thread的map(t.threadLocals),也就是说对于一个Thread实例,ThreadLocalMap是其内部的一个属性,在需要的时候,可以通过ThreadLocal创建或者获取,然后存放相应的值。我们看下Thread类的关键代码

public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    //省略了其他代码
}

可以看到,Thread中定义了属性threadLocals,但其初始化和使用的过程,都是通过ThreadLocal这个类来执行的。

ThreadLocal.get()

get是获取当前线程的对应的私有变量,是我们之前set或者通过initialValue指定的变量,其代码如下

    /**
     * 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)
                return (T)e.value;
        }
        return setInitialValue();
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

可以看到,其逻辑也比较简单清晰:

  • 获取当前线程的ThreadLocalMap实例
  • 如果不为空,以当前ThreadLocal实例为key获取value
  • 如果ThreadLocalMap为空或者根据当前ThreadLocal实例获取的value为空,则执行setInitialValue()

setInitialValue()内部如下:

  • 调用我们重写的initialValue得到一个value
  • 将value放入到当前线程对应的ThreadLocalMap中
  • 如果map为空,先实例化一个map,然后赋值KV

关键设计小结

代码分析到这里,其实对于ThreadLocal的内部主要设计以及其和Thread的关系比较清楚了:

  • 每个线程,是一个Thread实例,其内部拥有一个名为threadLocals的实例成员,其类型是ThreadLocal.ThreadLocalMap
  • 通过实例化ThreadLocal实例,我们可以对当前运行的线程设置一些线程私有的变量,通过调用ThreadLocal的set和get方法存取
  • ThreadLocal本身并不是一个容器,我们存取的value实际上存储在ThreadLocalMap中,ThreadLocal只是作为TheadLocalMap的key
  • 每个线程实例都对应一个TheadLocalMap实例,我们可以在同一个线程里实例化很多个ThreadLocal来存储很多种类型的值,这些ThreadLocal实例分别作为key,对应各自的value
  • 当调用ThreadLocal的set/get进行赋值/取值操作时,首先获取当前线程的ThreadLocalMap实例,然后就像操作一个普通的map一样,进行put和get

当然,这个ThreadLocalMap并不是一个普通的Map(比如常用的HashMap),而是一个特殊的,key为弱引用的map,这个我们后面再详谈

2.1.2 ThreadLocal内存模型

通过上一节的分析,其实我们已经很清楚ThreadLocal的相关设计了,对数据存储的具体分布也会有个比较清晰的概念。下面的图是网上找来的常见到的示意图,我们可以通过该图对ThreadLocal的存储有个更加直接的印象。

TheadLocal内存模型

我们知道Thread运行时,线程的的一些局部变量和引用使用的内存属于Stack(栈)区,而普通的对象是存储在Heap(堆)区。根据上图,基本分析如下:

  • 线程运行时,我们定义的TheadLocal对象被初始化,存储在Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的ThreadLocalRef
  • 当ThreadLocal的set/get被调用时,虚拟机会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,然后查看其对用的TheadLocalMap实例是否被创建,如果没有,则创建并初始化。
  • Map实例化之后,也就拿到了该ThreadLocalMap的句柄,然后如果将当前ThreadLocal对象作为key,进行存取操作
  • 图中的虚线,表示key对ThreadLocal实例的引用是个弱引用

3. 插曲:强引用/弱引用

java中的引用分为四种,按照引用强度不同,从强到弱依次为:强引用、软引用、弱引用和虚引用,如果不是专门做jvm研究,对其概念很难清晰的定义,我们大致可以理解为,引用的强度,代表了对内存占用的能力大小,具体体现在GC的时候,会不会被回收,什么时候被回收

ThreadLocal被用作TheadLocalMap的弱引用key,这种设计也是ThreadLocal被讨论内存泄露的热点问题,因此有必要了解一下什么是弱引用。

3.1 强引用

强引用虽然在开发过程中并不怎么提及,但是无处不在,例如我们在一个对象中通过如下代码实例化一个StringBuffer对象

StringBuffer buffer = new StringBuffer();

我们知道StringBuffer的实例通常是被创建在堆中的,而当前对象持有该StringBuffer对象的引用,以便后续的访问,这个引用,就是一个强引用。

对GC知识比较熟悉的可以知道,HotSpot JVM目前的垃圾回收算法一般默认是可达性算法,即在每一轮GC的时候,选定一些对象作为GC ROOT,然后以它们为根发散遍历,遍历完成之后,如果一个对象不被任何GC ROOT引用,那么它就是不可达对象,则在接下来的GC过程中很可能会被回收。

强引用最重要的就是它能够让引用变得强(Strong),这就决定了它和垃圾回收器的交互。具体来说,如果一个对象通过一串强引用链接可到达(Strongly reachable),它是不会被回收的。如果你不想让你正在使用的对象被回收,这就正是你所需要的。

3.2 软引用

软引用是用来描述一些还有用但是并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收返回之后进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。JDK1.2之后提供了SoftReference来实现软引用。

相对于强引用,软引用在内存充足时可能不会被回收,在内存不够时会被回收。

3.3 弱引用

弱引用也是用来描述非必须的对象的,但它的强度更弱,被弱引用关联的对象只能生存到下一次GC发生之前,也就是说下一次GC就会被回收。JDK1.2之后,提供了WeakReference来实现弱引用。

3.4 虚引用

虚引用也成为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个瑞祥是否有虚引用的存在,完全不会对其生存时间造成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是在这个对象被GC时收到一个系统通知。JDK1.2之后提供了PhantomReference来实现虚引用

4. 可能的内存泄露分析

了解了ThreadLocal的内部模型以及弱引用,接下来可以分析一下是否有内存泄露的可能以及如何避免。

4.1 内存泄露分析

根据上一节的内存模型图我们可以知道,由于ThreadLocalMap是以弱引用的方式引用着ThreadLocal,换句话说,就是ThreadLocal是被ThreadLocalMap以弱引用的方式关联着,因此如果ThreadLocal没有被ThreadLocalMap以外的对象引用,则在下一次GC的时候,ThreadLocal实例就会被回收,那么此时ThreadLocalMap里的一组KV的K就是null了,因此在没有额外操作的情况下,此处的V便不会被外部访问到,而且只要Thread实例一直存在,Thread实例就强引用着ThreadLocalMap,因此ThreadLocalMap就不会被回收,那么这里K为null的V就一直占用着内存

综上,发生内存泄露的条件是

  • ThreadLocal实例没有被外部强引用,比如我们假设在提交到线程池的task中实例化的ThreadLocal对象,当task结束时,ThreadLocal的强引用也就结束了
  • ThreadLocal实例被回收,但是在ThreadLocalMap中的V没有被任何清理机制有效清理
  • 当前Thread实例一直存在,则会一直强引用着ThreadLocalMap,也就是说ThreadLocalMap也不会被GC

也就是说,如果Thread实例还在,但是ThreadLocal实例却不在了,则ThreadLocal实例作为key所关联的value无法被外部访问,却还被强引用着,因此出现了内存泄露。

也就是说,我们回答了文章开头的第一个问题,ThreadLocal如果使用的不当,是有可能引起内存泄露的,虽然触发的场景不算很容易。

这里要额外说明一下,这里说的内存泄露,是因为对其内存模型和设计不了解,且编码时不注意导致的内存管理失联,而不是有意为之的一直强引用或者频繁申请大内存。比如如果编码时不停的人为塞一些很大的对象,而且一直持有引用最终导致OOM,不能算作ThreadLocal导致的“内存泄露”,只是代码写的不当而已!

4.2 TheadLocal本身的优化

进一步分析ThreadLocalMap的代码,可以发现ThreadLocalMap内部也是做了一定的优化的

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

可以看到,在set值的时候,有一定的几率会执行replaceStaleEntry(key, value, i)方法,其作用就是将当前的值替换掉以前的key为null的值,重复利用了空间。

5. ThreadLocal使用建议

通过前面几节的分析,我们基本弄清楚了ThreadLocal相关设计和内存模型,对于是否会发生内存泄露做了分析,下面总结下几点建议:

  • 当需要存储线程私有变量的时候,可以考虑使用ThreadLocal来实现
  • 当需要实现线程安全的变量时,可以考虑使用ThreadLocal来实现
  • 当需要减少线程资源竞争的时候,可以考虑使用ThreadLocal来实现
  • 注意Thread实例和ThreadLocal实例的生存周期,因为他们直接关联着存储数据的生命周期
    • 如果频繁的在线程中new ThreadLocal对象,在使用结束时,最好调用ThreadLocal.remove来释放其value的引用,避免在ThreadLocal被回收时value无法被访问却又占用着内存

其实对于ThreadLocalMap还有很多设计,关于其详细内容,可以参考文后参考文章的最后一篇

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容