并发——深入分析ThreadLocal的实现原理
1|0一、前言
这篇博客来分析一下ThreadLocal
的实现原理以及常见问题,由于现在时间比较晚了,我就不废话了,直接进入正题。
2|0二、正文
2|12.1 ThreadLocal是什么
在讲实现原理之前,我先来简单的说一说ThreadLocal
是什么。ThreadLocal
被称作线程局部变量,当我们定义了一个ThreadLocal
变量,所有的线程共同使用这个变量,但是对于每一个线程来说,实际操作的值是互相独立的。简单来说就是,ThreadLocal能让线程拥有自己内部独享的变量。举一个简单的例子:
上面的代码,运行结果如下(注:每次运行的结果可能不同):
通过上面的输出结果我们可以发现,线程1
和线程2
虽然使用的是同一个ThreadLocal
变量存储值,但是输出结果中,两个线程的值却互不影响,线程1
从1
输出到10
,而线程2
从100
输出到91
。这就是ThreadLocal
的功能,即让每一个线程拥有自己独立的变量,多个线程之间互不影响。
2|22.2 ThreadLocal的实现原理
下面我就就来说一说ThreadLocal
是如何做到线程之间相互独立的,也就是它的实现原理。这里我直接放出结论,后面再根据源码分析:每一个线程都有一个对应的Thread对象,而Thread类有一个成员变量,它是一个Map集合,这个Map集合的key就是ThreadLocal的引用,而value就是当前线程在key所对应的ThreadLocal中存储的值。当某个线程需要获取存储在ThreadLocal变量中的值时,ThreadLocal底层会获取当前线程的Thread对象中的Map集合,然后以ThreadLocal作为key,从Map集合中查找value值。这就是ThreadLocal
实现线程独立的原理。也就是说,ThreadLocal
能够做到线程独立,是因为值并不存在ThreadLocal
中,而是存储在线程对象中。下面我们根据ThreadLocal
中两个最重要的方法来确认这一点。
2|32.3 ThreadLocal中的get方法
get
方法的作用非常简单,就是线程向ThreadLocal
中取值,下面我们来看看它的源码:
上面的代码非常直观的验证了我之前说过的ThreadLocal
的实现原理。通过上面的代码,我们可以非常直观的看到,线程向ThreadLocal
中存放的值,最后都放入了线程自己的ThreadLocalMap
中,而这个map
的key
就是当前ThreadLocal
的引用。而ThreadLocal
中,获取线程的ThreadLocalMap
的方法getMap
的代码如下:
我们再看看Thread
类中的threadLocals
变量:
2|42.4 ThreadLocal中的set方法
下面再来看一看ThreadLocal
的set
方法的实现,set
方法用来使线程向ThreadLocal
中存放值(实际上是存放在线程自己的Map
中):
这就是set
方法的实现,比较简单。看完上面两个关键方法的实现,相信大家对ThreadLocal
的实现已经有了一个比较清晰的认识,下面我们来更加深入的分析ThreadLocal
,看看ThreadLocalMap
的一些实现细节。
2|52.5 ThreadLocalMap的中的弱引用
ThreadLocalMap
的实现其实就是一个比较普通的Map
集合,它的实现和HashMap
类似,所以具体的实现细节我们就不一一讲解了,这里我们只关注它最特别的一个地方,即它内部的节点Entry
。我们先来看看Entry
的代码:
可以看到,上面的Entry
比较特殊,它继承自WeakReference
类型,这是Java
实现的弱引用。在具体讲解前,我们先来介绍一下不同类型的引用:
强引用:这是Java中最常见的引用,在没有使用特殊引用的情况下,都是强引用,比如Object o = new Object()就是典型的强引用。能让程序员通过强引用访问到的对象,不会被JVM垃圾回收,即使内存空间不够,JVM也不会回收这些对象,而是抛出内存溢出异常;
软引用:软引用描述的是一些还有用,但不是必须的对象。被软引用所引用的对象,也不会被垃圾回收,直到JVM将要发生内存溢出异常时,才会将这些对象列为回收对象,进行回收。在JDK1.2之后,提供了SoftReference类实现软引用;
弱引用:弱引用描述的是非必须的对象,被弱引用所引用的对象,只能生存到下一次垃圾回收前,下一次垃圾回收来临,此对象就会被回收。在JDK1.2之后,提供了WeakReference类实现弱引用(也就是上面Entry继承的类);
虚引用:这是最弱的一种引用关系,一个对象是否有虚引用,完全不会对其生存时间产生影响,我们也不能通过一个虚引用访问对象,使用虚引用的唯一目的就是,能在这个对象被回收时,受到一个系统的通知。JDK1.2之后,提供了PhantomReference实现虚引用;
介绍完各类引用的概念,我们就可以来分析一下Entry
为什么需要继承WeakReference
类了。从代码中,我们可以看到,Entry
将key
值,也就是ThreadLocal
的引用传入到了WeakReference
的构造方法中,也就是说在ThreadLocalMap
中,key
的引用是弱引用。这表明,当没有其他强引用指向key
时,这个key
将会在下一次垃圾回收时被JVM
回收。
为什么需要这么做呢?这么做的目的自然是为了有利于垃圾回收了。如果了解过JVM
的垃圾回收算法的应该知道,JVM
判断一个对象是否需要被回收,判断的依据是这个对象还能否被我们所使用,举个简单的例子:
上面的代码中,我们创建了一个对象,并使用强引用o
指向它,然后我们将o
置为空,这个时候刚刚创建的对象就丢失了,因为我们无法通过任何引用找到这个对象,从而使用它,于是这个对象就需要被回收,这种判断依据被称为可达性分析。关于JVM
的垃圾回收算法,可以参考这篇博客:Java中的垃圾回收算法详解。
好,回归正题,我们开始分析为什么ThreadLocalMap
需要让key
使用弱引用。假设我们创建了一个ThreadLocal
,使用完之后没有用了,我们希望能够让它被JVM
回收,于是有了下面这个过程:
我们在使用完ThreadLocal
之后,解除对它的强引用,希望它被JVM
回收。但是JVM
无法回收它,因为我们虽然在此处释放了对它的强引用,但是它还有其它强引用,那就是Thread
对象的ThreadLocalMap
的key
。我们之前反复说过,ThreadLocalMap
的key
就是ThreadLocal
对象的引用,若这个引用是一个强引用,那么在当前线程执行完毕,被回收前,ThreadLocalMap
不会被回收,而ThreadLocalMap
不会被回收,它的key
引用的ThreadLocal
也就不会回收,这就是问题的所在。而使用弱引用就可以保证,在其他对ThreadLocal的强引用解除后,ThreadLocalMap对它的引用不会影响JVM对它进行垃圾回收。这就是使用弱引用的原因。
2|62.6 ThreadLocal造成的内存溢出问题
上面描述了对ThreadLocalMap
对key
使用弱引用,来避免JVM
无法回收ThreadLocal
的问题,但是这里却还有另外一个问题。我们看上面Entry
的代码发现,key
值虽然使用的弱引用,但是value使用的却是强引用。这会造成一个什么问题?这会造成key被JVM回收,但是value却无法被收,key对应的ThreadLocal被回收后,key变为了null,但是value却还是原来的value,因为被ThreadLocalMap所引用,将无法被JVM回收。若value
所占内存较大,线程较多的情况下,将持续占用大量内存,甚至造成内存溢出。我们通过一段代码演示这个问题:
上面的代码多次创建所占内存非常大的对象,并在创建后,立即解除对象的强引用,让对象可以被JVM
回收。按道理来说,上面的代码运行应该不会发生内存溢出,因为我们虽然创建了多个大对象,占用了大量空间,但是这些对象立即就用不到了,可以被垃圾回收,而这个对象被垃圾回收后,对象的id
,数组,和threadLocal
成员都会被回收,所以所占内存不会持续升高,但是实际运行结果如下:
可以看到,很快就发生了内存溢出异常。为什么呢?需要注意到,在TestClass
的构造方法中,我们将数组arr
放入了ThreadLocal
对象中,也就是被放进了当前线程的ThreadLocalMap
中,作为value
存在。我们前面说过,ThreadLocalMap
的value
是强引用,这也就意味着虽然ThreadLocal
可以被正常回收,但是作为value
的大数组无法被回收,因为它仍然被ThreadLocalMap
的强引用所指向。于是TestClass
对象的超大数组就一种在内存中,占据大量空间,我们连续创建了多个TestClass
,内存很快就被占满了,于是发生了内存溢出。而JDK
的开发人员自然发现了这个问题,于是有了下面这个解决方案:
上面的代码中,我们在将t
置为空时,先调用了ThreadLocal
对象的remove
方法,这样做了之后,再看看运行结果:
做了上面的修改后,没有再发生内存溢出异常,程序正常执行完毕。这是为什么呢?ThreadLocal
的remove
方法究竟有什么作用。其实remove
方法的作用非常简单,执行remove
方法时,会从当前线程的ThreadLocalMap
中删除key
为当前ThreadLocal
的那一个记录,key
和value
都会被置为null,这样一来,就解除了ThreadLocalMap
对value
的强引用,使得value
可以正常地被JVM
回收了。所以,今后如果我们确认不再使用的ThreadLocal
对象,一定要记得调用它的remove
方法。
我们之前说过,如果我们没有调用remove
方法,那就会导致ThreadLocal
在使用完毕后,被正常回收,但是ThreadLocalMap
中存放的value
无法被回收,此时将会在ThreadLocalMap
中出现key
为null
,而value
不为null
的元素。为了减少已经无用的对象依旧占用内存的现象,ThreadLocal
底层实现中,在操作ThreadLocalMap
的过程中,线程若检测到key
为null
的元素,会将此元素的value
置为null
,然后将这个元素从ThreadLocalMap
中删除,占用的内存就可以让JVM
将其回收。比如说在getEntry
方法中,或者是Map
扩容的方法中等。
3|0三、总结
ThreadLocal
实现线程独立的方式是直接将值存放在Thread
对象的ThreadLocalMap
中,Map
的key
就是ThreadLocal
的引用,且为了有助于JVM
进行垃圾回收,key
使用的是弱引用。在使用ThreadLocal
后,一定要记得调用remove
方法,有助于JVM
对value
的回收。
4|0四、参考
- 《深入理解Java虚拟机(第二版)》
- https://mp.weixin.qq.com/s/Y24LQwukYwXueTS6NG2kKA
__EOF__

本文链接:https://www.cnblogs.com/tuyang1129/p/12713815.html
关于博主:在互联网洋流中垂死挣扎,但依旧乐观的Java小菜鸟一枚!
版权声明:转载博客请注明出处,并附上原文链接!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构