【Java面试】并发场景中,ThreadLocal会造成内存泄漏吗?
ThreadLoca在并发场景中,应用非常多。前几天有位小伙伴问我一个问题,说ThreadLocal是不是真的会造成内存泄漏?今天给大家做一个分享,个人见解,仅供参考。如果大家有其他见解可以在评论区讨论。
【导航条:基本原理】
(转场,屏幕黑底白字,正中央出现“1 ThreadLocal的基本原理”)
1、ThreadLocal的基本原理
考虑到很多小伙伴可能还不太了解ThreadLocal,我先简单介绍一下ThreadLocal。在多线程并发访问同一个共享变量的情况下,如果不做同步控制的话,就可能会导致数据不一致的问题,所以,我们需要使用synchronized加锁来解决。
而ThreadLocal换了一个思路来处理多线程的情况,
(开始播放动画)
ThreadLocal本身并不存储数据,它使用了线程中的threadLocals属性,threadLocals的类型就是在ThreadLocal中的定义的ThreadLocalMap对象,当调用ThreadLocal的set(T value)方法时,ThreadLocal将自身的引用也就是this作为Key,然后,把用户传入的值作为Value存储到线程的ThreadLocalMap中,这就相当于每个线程的读写操作都是基于线程自身的一个私有副本,线程之间的数据是相互隔离的,互不影响。
(结束播放动画)
这样一来基于ThreadLocal的操作也就不存在线程安全问题了。它相当于采用了用空间来换时间的思路,从而提高程序的执行效率。
【导航条:四种对象引用】
(转场,屏幕黑底白字,正中央出现“2 四种对象引用”)
2、四种对象引用
在ThreadLocalMap内部,维护了一个Entry数组table的属性,用来存储键值对的映射关系,来看这样一段代码片段:
(开始播放动画)
static class ThreadLocalMap { ... private Entry[] table; static class Entry implements WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ... }
(结束播放动画)
Entry将ThreadLocal作为Key,值作为Value保存,它继承自WeakReference,注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」。有的小伙伴可能对「弱引用」不太熟悉,这里再介绍一下Java的四种引用关系。
在JDK1.2之后,Java对引用的概念做了一些扩充,将引用分为“强”、“软”、“弱”、“虚”四种,由强到弱依次为:
(开始播放动画)
强引用:指代码中普遍存在的赋值行为,如:Object o = new Object(),只要强引用关系还在,对象就永远不会被回收。
软引用:还有用处,但不是必须存活的对象,JVM会在内存溢出前对其进行回收,例如:缓存。
弱引用:非必须存活的对象,引用关系比软引用还弱,不管内存是否够用,下次GC一定回收。
虚引用:也称“幽灵引用”、“幻影引用”,最弱的引用关系,完全不影响对象的回收,等同于没有引用,虚引用的唯一的目的是对象被回收时会收到一个系统通知。
(结束播放动画)
这个描述还是比较官方的,简单总结一下,大家应该都追过剧,强引用就好比是男主角,怎么都死不了。软引用就像女主角,虽有一段经历,还是没走到最后。弱引用就是男二号,注定用来牺牲的。虚引用就是路人甲了。
【导航条:内存泄漏原因】
(转场,屏幕黑底白字,正中央出现“3 造成内存泄漏的原因”)
3、造成内存泄漏的原因
内存泄漏和ThreadLocalMap中定义的Entry类有非常大的关系。
(开始展示动画)
这个动画完整地展示了ThreadLocal中对象引用的关系,需要这张高清图的小伙伴可以在评论区留言。
由于ThreadLocal对象是弱引用,如果外部没有强引用指向它,它就会被GC回收,导致Entry的Key为空(null),如果这时Value外部也没有强引用指向它,那么Value就永远也访问不到了,按理也应该被GC回收,但是由于Entry对象还在强引用Value,导致Value无法被回收,这时「内存泄漏」就发生了,Value成了一个永远也无法被访问,但是又无法被回收的对象。
(结束展示动画)
Entry对象属于ThreadLocalMap,ThreadLocalMap又属于Thread,如果线程本身的生命周期很短,短时间内就会被销毁,那么「内存泄漏」立刻就会得到解决,只要线程被销毁,Value也会随之被回收。
问题是,线程本身是非常珍贵的计算机资源,很少会去频繁的创建和销毁,一般都是通过线程池来使用,这就将线程的生命周期大大拉长,「内存泄漏」的影响也会越来越大。
最后,一句话总结一下。
(开始展示动画)
threadLocals对象中的Entry对象不再使用后,如果没有及时清除Entry对象 ,而程序自身也无法通过垃圾回收机制自动清除,就可能导致内存泄漏。
(结束展示动画)
【导航条:如何避免】
(转场,屏幕黑底白字,正中央出现“4 如何避免内存泄漏?”)
4、如何避免内存泄漏?
不要听到「内存泄漏」就不敢使用ThreadLocal,只要规范化使用是不会有问题的。我给大家支几个招:
(开始展示动画)
1、每次使用完ThreadLocal都记得调用remove()方法清除数据。
2、将ThreadLocal变量尽可能地定义成static final,避免频繁创建ThreadLocal实例。这样也就保证程序一直存在ThreadLocal的强引用,也能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的Value值,进而清除掉。
(结束展示动画)
当然,就是使用不规范,ThreadLocal内部也做了一些优化,比如:
(开始展示动画)
1、调用set()方法时,ThreadLocal会进行采样清理、全量清理,扩容时还会继续检查。
2、调用get()方法时,如果没有直接命中或者向后环形查找时也会进行清理。
3、调用remove()时,除了清理当前Entry,还会向后继续清理。
所以,我真的希望小伙伴们,不要被制造焦虑的面试官们卷得太深。听懂的小伙伴,请关注点个赞,下次不迷路。
扫描下方二维码即可免费领取需要更多干货内容,还有海量面试资料,只弹干货不惨水!
本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!
如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。