ThreadLocal真的会造成内存泄漏吗?

开发 前端
在多线程并发访问同一个共享变量的情况下,如果不做同步控制的话,就可能会导致数据不一致的问题,所以,我们需要使用synchronized加锁来解决。

ThreadLoca在并发场景中,应用非常多。前几天有位小伙伴问我一个问题,说ThreadLocal是不是真的会造成内存泄漏?今天给大家做一个分享。

1、ThreadLocal的基本原理

考虑到很多小伙伴可能还不太了解ThreadLocal,我先简单介绍一下ThreadLocal。在多线程并发访问同一个共享变量的情况下,如果不做同步控制的话,就可能会导致数据不一致的问题,所以,我们需要使用synchronized加锁来解决。

而ThreadLocal换了一个思路来处理多线程的情况。

ThreadLocal本身并不存储数据,它使用了线程中的threadLocals属性,threadLocals的类型就是在ThreadLocal中的定义的ThreadLocalMap对象,当调用ThreadLocal的set(T value)方法时,ThreadLocal将自身的引用也就是this作为Key,然后,把用户传入的值作为Value存储到线程的ThreadLocalMap中,这就相当于每个线程的读写操作都是基于线程自身的一个私有副本,线程之间的数据是相互隔离的,互不影响。

这样一来基于ThreadLocal的操作也就不存在线程安全问题了。它相当于采用了用空间来换时间的思路,从而提高程序的执行效率。

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、造成内存泄漏的原因

内存泄漏和ThreadLocalMap中定义的Entry类有非常大的关系。

这个动画完整地展示了ThreadLocal中对象引用的关系,需要这张高清图的小伙伴可以在评论区留言。

由于ThreadLocal对象是弱引用,如果外部没有强引用指向它,它就会被GC回收,导致Entry的Key为空(null),如果这时Value外部也没有强引用指向它,那么Value就永远也访问不到了,按理也应该被GC回收,但是由于Entry对象还在强引用Value,导致Value无法被回收,这时「内存泄漏」就发生了,Value成了一个永远也无法被访问,但是又无法被回收的对象。

Entry对象属于ThreadLocalMap,ThreadLocalMap又属于Thread,如果线程本身的生命周期很短,短时间内就会被销毁,那么「内存泄漏」立刻就会得到解决,只要线程被销毁,Value也会随之被回收。

问题是,线程本身是非常珍贵的计算机资源,很少会去频繁的创建和销毁,一般都是通过线程池来使用,这就将线程的生命周期大大拉长,「内存泄漏」的影响也会越来越大。

最后,一句话总结一下。

threadLocals对象中的Entry对象不再使用后,如果没有及时清除Entry对象 ,而程序自身也无法通过垃圾回收机制自动清除,就可能导致内存泄漏。

4、如何避免内存泄漏?

不要听到「内存泄漏」就不敢使用ThreadLocal,只要规范化使用是不会有问题的。我给大家支几个招:

  1. 每次使用完ThreadLocal都记得调用remove()方法清除数据。
  2. 将ThreadLocal变量尽可能地定义成static final,避免频繁创建ThreadLocal实例。这样也就保证程序一直存在ThreadLocal的强引用,也能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的Value值,进而清除掉。

当然,就是使用不规范,ThreadLocal内部也做了一些优化,比如:

  1. 调用set()方法时,ThreadLocal会进行采样清理、全量清理,扩容时还会继续检查。
  2. 调用get()方法时,如果没有直接命中或者向后环形查找时也会进行清理。
  3. 调用remove()时,除了清理当前Entry,还会向后继续清理。
责任编辑:姜华 来源: Tom弹架构
相关推荐

2016-10-31 20:56:57

Javascript闭包内存泄漏

2018-10-25 15:24:10

ThreadLocal内存泄漏Java

2021-08-10 09:58:59

ThreadLocal内存泄漏

2020-09-10 07:40:28

ThreadLocal内存

2022-09-22 13:56:56

线程Java

2021-02-18 16:53:44

内存ThreadLocal线程

2024-03-22 13:31:00

线程策略线程池

2022-10-18 08:38:16

内存泄漏线程

2023-03-01 09:39:05

2021-04-23 07:27:31

内存分配CPU

2022-03-25 09:39:50

LinuxLinux top

2022-04-07 07:41:18

String字符串内存泄漏

2021-06-02 07:57:48

内存管理

2023-05-29 07:17:48

内存溢出场景

2021-07-13 06:42:58

JavaEquals方法

2018-11-20 09:37:19

Java内存模型

2011-06-17 10:50:32

Windows 8Windows Pho

2012-02-22 21:28:58

内存泄漏

2023-12-18 10:45:23

内存泄漏计算机服务器

2019-07-17 15:45:24

Spark内存Java
点赞
收藏

51CTO技术栈公众号