深入理解Threadlocal 关于内存泄漏的思考

不知道经常使用  Threadlocal  的朋友有没有意识到内存泄漏这一点。

什么是内存泄漏呢?对象已经没有在其它地方被使用了,但是垃圾回收器没办法移除它们,因为还在被引用着。

我不用的对象,又不能被垃圾回收,就会造成内存泄漏。不了解垃圾回收的朋友看这篇文章:垃圾回收的细节

简单的拿个图表示下:

如果你了解垃圾回收机制,活着看过周志明老师的 深入理解java虚拟机 第二版, 你肯定 知道

强,软,弱,虚。四种引用关系。在进行GC时,只有强引用关系存在的对象才不会被垃圾回收。

而 ThreadLocalMapl里的 enter对象 继承了  jdk  WeakReference (弱引用API)提供的  的key ,也就是 ThreadLocal 取的是 WeakReference提供的弱引用对象,所以在GC时, ThreadLocal 会被垃圾回收期回收掉, Entry对象的key就为null了,然后 value 却是强引用 无法回收。

先上代码再上图~

[java] view plain copy

  1. static class ThreadLocalMap {  
  2. static class Entry extends WeakReference<ThreadLocal<?>> {  
  3. /** The value associated with this ThreadLocal. */
  4.            Object value;  
  5.            Entry(ThreadLocal<?> k, Object v) {  
  6. super(k);  
  7.                value = v;  
  8.            }  
  9.        }  

super ( k );  k 是从 weakReference里得到的,弱引用无疑,value ,自己的Object ,一般都是强引用。

把它们的 堆栈图 画出来,让大家更好的理解:

这个图应该阐述得很清楚了~

每个Thread都有自己的 一个 ThreadLocalMap。  key 是 TreadLocal 实例对象, value 就是 你要保存的那个 变量。

图中红色部分描述的了 ThreadLocalMap 是通过WeakReference 包装了 TreadLocal ,取的是 TreadLocal的弱引用 对象。

那么在GC 的时候就会造成 TreadLocal 肯定就会被回收掉。 Entry对象的key就为null了,然后 value 却是强引用 无法回收。

如果这个方法又长时间不结束的话,就有会这么一条 强引用的 GCROOT 引用链 的存在:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value ; value 就一直不会被回收, 因为它的 另一半 key 已经不存在了,所以它也不会被调用。 这就造成了内存泄漏。

但是!这可是大名鼎鼎的JDK诶,1.9都出来了,肯定考虑到这个点了,于是在1.5的时候加入了remove 方法 解决这个问题 ; 

后来又针对:怕部分程序员还是忘记调用remove 方法,又在get方法 中做了优化, 我们看看源码,是哪里优化了:

这里就不贴源码了, 认真的朋友可以进入自己的 开发工具 跟着下面 的 注释,一个一个的点进去看,一目了然。

源码的进入姿势是 ThreadLocal.class 的 get( )  -> ThreadLocalMap 的 getEntry  ( this ) 方法, 

当key 不满足 判断条件时, 进入 getEntryAfterMiss(key, i, e)  方法 ;

当key == null  ->  expungeStaleEntry(i);  如果 k == null 时,  e.value = null;

看到这里,意思就是在Threadlocal 在调用get 方法的时候,如果key 是 null  就会把 value 的强引用关系清除掉。

这样 Threadlocal  被垃圾回收掉的时候  保存的 副本变量 也会被 垃圾回收 从而避免了 部分次数的 内存泄漏。

但这并不能,完全的避免内存泄漏, 仍然需要我们在 调用set  方法后  显示的  调用remove()方法。

remove 方法 的源码 , 其实就是清除了 entry 对象的引用 关系,

然后又调用了 expungeStaleEntry(i) 方法,key ==null时 , e.value =null ;

所以我们应该有意识的形成良好的编程习惯/规范,在使用完ThreadLocal之后,记得调用一下remove方法。从而避免内存泄漏。

到这里,ThreadLocal 造成内存泄漏的原因以及解决办法以及分析完了。

上一篇中 <一>深入理解面试常问的Threadlocal的实现原理 提到了 主题内容的第三部分也分析完了。

我们再来进行主题四:思考 和 总结 学习的 这个 ThreadLocal;

先来思考一个问题: 我们知道了内存泄漏是因为  ThreadLocalMap 中 entry 对象的 key 去的是 ThreadLocal的弱引用对象。

那我是不是将  ThreadLocal 的弱引用 换成 强引用 就不会引起内存泄漏了呢?

于是我们拿 key 取弱引用对象 跟 强引用对象 做个对比,再分析分析优缺点~

key 是 强引用:

如果我们从 ThreadLocal 里面 已经取到了我们想要的  线程副本 value ,我们是不是就希望 ThreadLocal能够被垃圾回收掉呢? 但是因为 ThreadLocalMap  中的 entry 还持有对  ThreadLocal  的强引用。 所以导致 ThreadLocal 迟迟都不能被垃圾回收。所以value 也不能被垃圾回收,从而造成了 entry 对象 发生内存泄漏。

key 是 弱引用:

首先我们看key~  ThreadLocal 被垃圾回收时,就算 ThreadLocalMap  持有  ThreadLocal  的引用也没有关系,这是一种弱引用关系, 即使我们没有手动的将  ThreadLocal 设置为 null ,垃圾回收器还是会将 ThreadLocal 回收掉。

再看value~ value 在调用get  remove 方法的时候也会被垃圾回收。

对比分析后,我们可以发现,如果key 是弱引用,我调用 remove 方法 就能避免value 对象的内存泄漏。

 如果key  是强引用,我用完了 ThreadLocal 我还得将 ThreadLocal 设置为null,value也设置为null

最后发现:哦~造成内存泄漏的根本原因并不是弱引用关系所导致的,真正的原因是:(这里我们提到一个生命周期的概念)

ThreadLocalMap和Thread的生命周期一样长,而 ThreadLocal  实际上较短(因为我用完就不需要它了)。

在没有手动的删除key 的情况下,就会造成泄漏, JDK 现在用的弱引用 优化了 在程序员失误的情况下,我只内存泄漏value,

并且提供了不泄漏value 的 API 方法 :显示调用 remove方法。而用强引用, 那我key 和 value 全部都可能内存泄漏。

那么不知道大家是否想起了其它情况下的内存泄漏,比如集合类,数据库资源那些的。

其根本问题全在这儿:引用方和被引用方的生命周期长短不一致导致的。 这算是对多种情况的一个上层抽象吧~

这么分析了一波 ThreadLocal 能给我们带来什么。

1、了解了 ThreadLocal 的实现原理,从而能更好的使用 ThreadLocal ,能避免内存泄漏的情况。

2、能规范我们的编码习惯,并抽象出了内存泄漏的原因,以后编码时有意识考虑这些问题。

3、ThreadLocal 能实现每个线程都有一份变量副本,其实就是空间换时间的设计思路,因为每个线程都有个ThreadLocalMap

从而实现了   另一种意义上的 “无锁编程”。

4、  你懂的~

最后 ThreadLocal  就跟加锁后要释放锁一样的, 用完记得调用 remove 方法。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏ImportSource

设计ThreadLocal的那段日子

假设现在让你去实现一个连接类,要支持多个线程访问,同时每个线程独占一个Connection? 这时候你会怎么实现? 也许这会你想到了给每个线程传递一个Conne...

3476
来自专栏Android 研究

OKHttp源码解析(五)--OKIO简介及FileSystem

okio是由square公司开发的,它补充了java.io和java.nio的不足,以便能够更加方便,快速的访问、存储和处理你的数据。OKHttp底层也是用该库...

1822
来自专栏轮子工厂

务实基础篇--Java内存模型及GC原理

堆是Java代码可及的内存,留给开发人员使用的;非堆是JVM留给自己用的,包含方法区、JVM内部处理或优化所需的内存(如 JIT Compiler,Just-i...

1612
来自专栏码匠的流水账

No thread-bound request found异常

本文主要研究下spring mvc的No thread-bound request found异常

2330
来自专栏大闲人柴毛毛

揭秘ThreadLocal

ThreadLocal是开发中最常用的技术之一,也是面试重要的考点。本文将由浅入深,介绍ThreadLocal的使用方式、实现原理、内存泄漏问题以及使用场景。...

40012
来自专栏Coding01

学几个 Laravel Eloquent 方法和技巧

我第一次寻找所谓的 Laravel 框架的时候,我的其中一个目标就是要找:利用最简单的操作数据库的方法。后来目标就停在了 Eloquent ORM 上。

921
来自专栏大数据学习笔记

Google sshxcute优化版+二次封装类

1.Google sshxcute优化版 源码下载: https://gitee.com/chengyuqiang/sshxcute 2.二次封装 pack...

3778
来自专栏Java架构沉思录

ThreadLocal父子线程数据传递方案

介绍InheritableThreadLocal之前,假设对 ThreadLocal 已经有了一定的理解,比如基本概念、原理。在讲解之前我们先列举有关Threa...

1503
来自专栏抠抠空间

爬虫之Xpath详解

XPath 是一门在 XML 文档中查找信息的语言。XPath 可用来在 XML 文档中对元素和属性进行遍历。

882
来自专栏伦少的博客

打印(获取)HDFS路径下所有的文件名(包括子目录下的)

自己有个需求,如题,需要获取HDFS路径下所有的文件名,然后根据文件名用Spark进行后续操作。想了一下用Spark好像不太容易获取到,还要递归的去获取子目录下...

2911

扫码关注云+社区

领取腾讯云代金券