关于ThreadLocal内存泄露的备忘

还记得第一次接触到ThreadLocal可能导致内存泄露的问题是有一次面试的时候被问到了ThreadLocal的缺陷是什么。当然由于后来没有面试官的联系方式很遗憾也一直没能确认所谓的缺陷是不是就是可能导致内存泄漏,不过后来发现虽然当时弄明白了可是过段时间又搞忘记了这个问题,所以特别记录下来做个备忘吧。

ThreadLocal从名字上来说就很好理解,就是用于线程(Thread)私有(Local)的存储结构,这种结构能够使得线程能够使用只有自己能够访问和修改的变量,从而实现多个线程之间的资源互相隔离,达到安全并发的目的。

也因此,ThreadLocal作为线程并发中的一种资源使用方式,得到了很广泛的应用,比如Spring MVC、Hibernate等。
不过值得一提的是,通常有人会讲ThreadLocal和synchronised等放在一起,作为形成安全并发的手段之一。其实我觉得这是比较容易使人误导的,因为两者的目的性完全不一样。
ThreadLocal主要的是用于独享自己的变量,避免一些资源的争夺,从而实现了空间换时间的思想。
而synchronised则主要用于临界(冲突)资源的分配,从而能够实现线程间信息同步,公共资源共享等,所以严格来说synchronised其实是能够实现ThreadLocal所需要的达到的效果的,只不过这样会带来资源争夺导致并发性能下降,而且还有synchronised、线程切换等一些可能不必要的开销。


对于ThreadLocal而言,其实使用起来有点像基础类型的装箱类型的感觉(个人觉得其实也可以算是一种装饰器模式的使用?),具体的使用就不在啰嗦了。下面就看看这次备忘的重点,如何导致内存泄漏的。

其实网上有的文章已经讲的听清楚的,觉得有张图特别好先引用到这里,来源于ThreadLocal可能引起的内存泄露

[ThreadLocal可能引起的内存泄露](http://www.cnblogs.com/onlywujun/p/3524675.html)

所以简单的说,主要原因就是在于TreadLocal中用到的自己定义的Map(和常用的Map接口不同)中,使用的Key值是一个WeakReference类型的值(弱引用会在下一次GC时马上释放而不管是否被引用)。那么如果这个Key在GC时被释放了,就会导致Value永远都不会被调用到,但是如果线程不结束,又一直存在。

因为可能不熟悉这部分内容的同学(例如几周以后的我)会感觉有点迷糊为什么这个图是这样的,就具体再解释一下细节点:

  • 首先当然是看一下我们的主角ThreadLocal类,只保留了几个重点的地方,特别的是内部静态类的ThreadLocalMap是ThreadLocal自己实现的一个Map,而这个Map用使用了ThreadLocal作为了一个弱引用的Key(也就是主要问题点)。
    p.s.不知道各位第一次看的时候会不会跟我一样有种我是老子的儿子的同时又是老子的老子的感觉,哈哈哈
public class ThreadLocal<T> {
    
    //  获取Thread里面的Map
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    // (敲黑板)
    // 这里是重点!!!
    static class ThreadLocalMap {
        
        // 这里是凶器!!!
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        ... 
    }
    ... 
 }
  • 接着不得不说的就是我们的大佬Thread类,里面关于ThreadLocal部分的内容主要是这样滴。我们可以看到这里主要是声明了ThreadLocal里面的Map作为类变量来提供给线程使用的。也正式因为如此,才会在ThreadLocal里面的getMap方法是拉取的Thread里面的Map。
    p.s. 感觉确实有点绕
public class Thread implements Runnable {

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. 
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  • 于是到这里我们就明白了,其实每个Thread里面都有一个Map,Map里面的Key是ThreadLocal类的一个实例,之所以会比较混淆主要还是因为这里的Map又是ThreadLocal里面的一个内部静态类。

所以到这里其实有两个问题是暂时还没想通的,也希望有各位大佬指点一二:

  1. TreadLocalMap 其实是可以抽取成单独的类的?这样就使得逻辑和嵌套关系没有这么绕的感觉。
  2. 为什么只有Key要设计成WeakReference而不是Key和Value都是,或者这里为什么要设置弱引用?如果为了保护内存空间其实两者都是弱引用更好吧,是不是有什么其它考虑?

回归到内存泄露是因为WeakReference Key的问题,当然,Java的各位大佬肯定早就想到这个问题了,可以看到人家注释里面是这么说的,大意就是如果key==null的时候,就可以认为这个值无效了,可以调用expunged进行清理:

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

而这个expungeStaleEntry方法在get、set时都会有间接的调用,而且remove方法中也会显示的调用,这也就是为什么有的文章中说通过在线程调用完成之后,通过调用remove方法能有效的杜绝该泄露问题的原因。

当然简单来说理解到这里就基本明了内存泄露的原因,但是其实再深入一点来说,如果泄露的原因是Key被释放,而Value没有释放,那么是否一定会有泄露呢?
答案当然是否定的,因为如果是一般的线程场景中,除了会调用expungeStaleEntry来进行清理,最差,在线程结束之时,自然也就消除了引用从而使得Value得以GC回收。

所以,会不会有线程一直不结束的场景呢?
当然答案是肯定的,最简单来说线程只要一直在wait就不会结束了,不过这种场景下其实和泄露也没啥关系的感觉。
其实最常用的线程一直不结束的场景,自然就是线程池了。因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了value可能造成累积的情况。具体的模拟可以参考: 深入理解ThreadLocal的"内存溢出"


最后来做个总结吧,可能泄露的场景仅且仅在:

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

推荐阅读更多精彩内容