学习ThreadLocal

起因

为什么要写这篇文章?作为一个java程序员,你会发现在很多项目中都会使用这个工具,不仅如此,ThreadLocal还是面试经常被问到的一个考点,由此可见ThreadLocal的重要性。我自己也在学习的过程中使用过ThreadLocal,但只停留在别人教我这里要使用,我就使用的层面,对于为什么要使用ThreadLocal以及它的工作原理都是懵懵懂懂的状态。因此想要借此文章系统整理一下关于ThreadLocal的知识点,从而加深对它的理解

问题:ThreadLocal有什么用?

当你看到别人使用ThreadLocal时,你可能会问,为啥要使用ThreadLocal呢?因此,我们第一步需要知道ThreadLocal有什么用,它能解决什么问题。

ThreadLocal最主要的用途就是用来保证线程间数据的相互隔离。什么意思呢?简单来说就是在多线程环境下,每个线程从ThreadLocal中获取的数据都是自己之前存放进去的数据,而不会是其他线程的数据。基于此特性,在很多场景下都能使用ThreadLocal,比如层与层间进行参数传递,这样只要在一层中向ThreadLocal中存入数据,在其他层就可以直接获取数据,而不用在方法中显式传参。其实我们大部分的使用场景都是进行参数传递。

问题:ThreadLocal是如何工作的?

在了解了ThreadLocal的使用场景后,我们继续深入源码来了解ThreadLocal的工作原理,学习人家是如何保证线程间的数据隔离的。这里我们着重关注的是ThreadLocal的set()和get()方法(当然从中会牵扯出其他相关的方法)。

java 复制代码
 public void set(T value) {
     Thread t = Thread.currentThread();
     ThreadLocalMap map = getMap(t);
     if (map != null) {
         map.set(this, value);
     } else {
         createMap(t, value);
     }
 }

通过以上代码可以大概知道的是,set方法的底层会先获取当前线程,接着通过同类方法getMap获取一个ThreadLocalMap对象,最后以ThreadLocal对象为Key,要存放的数据为Value存放到ThreadLocalMap中。听完你可能脑袋里会有点晕晕的感觉,别急,我们接着往下看。首先我们要解决的问题是ThreadLocalMap是个什么东西?

我们先来看看ThreadLocal中的源码

java 复制代码
 static class ThreadLocalMap {
     static class Entry extends WeakReference<ThreadLocal<?>> {
         Object value;
 ​
         Entry(ThreadLocal<?> k, Object v) {
             super(k);
             value = v;
         }
     }
     ...
 }

我们再来看看getMap()的源码

java 复制代码
 ThreadLocalMap getMap(Thread t) {
     return t.threadLocals;
 }

通过以上源码大概可以知道以下信息:ThreadLocalMap是ThreadLocal中的一个静态内部类,并且每个线程中都有一个ThreadLocalMap属性。

以下是Thread中的源码

java 复制代码
 ThreadLocal.ThreadLocalMap threadLocals = null;

至此,我们可以简单梳理一下调用set方法的整个过程,用一个流程图来表示

你可能还会有疑问:为什么不直接用一个Map,在这个Map中Key存每个线程,Value存储每个线程对应的值呢?

这样做也能做到线程间数据的隔离,但是这种做法有一个问题:每个线程最多存放一个值,后面存放的值都会被覆盖。而jdk中实现的ThreadLocal,每个线程都拥有一个ThreadLocalMap,因此每个线程可以存放多个值,只需要new 一个对应的ThreadLocal即可

java 复制代码
 ThreadLocal<User>tl1 = new ThreadLocal<>();
 ThreadLocal<IdCard> tl2 = new ThreadLocal<>();
 ​
 tl1.set(new User());
 tl2.set(new IdCard());

我们再来研究get方法,先来看下get方法的源码

java 复制代码
 public T get() {
     Thread t = Thread.currentThread();
     ThreadLocalMap map = getMap(t);
     if (map != null) {
         ThreadLocalMap.Entry e = map.getEntry(this);
         if (e != null) {
             @SuppressWarnings("unchecked")
             T result = (T)e.value;
             return result;
         }
     }
     return setInitialValue();
 }

在有了上面的基础后,相信大家能够轻易的读懂这部分源码。一个很简单的逻辑:取出线程对应的ThreadLocalMap,再通过当前ThreadLocal取出对应的value,如果不存在map或Entry就返回一个默认值。这里有意思的是第5行代码,通过获取this对应的Entry来防止空指针异常。

ThreadLocal中存在的问题

很多时候如果我们不正确的使用ThreadLocal可能会造成严重的后果,最为熟知的就是内存泄露问题。

这是因为ThreadLocalMap中的key是弱引用(请看上述ThreadLocalMap源码),当外部不存在强引用时,就会被垃圾回收机制自动回收,但是value是一个强引用,只有当前线程结束运行后才会被回收,如果这些线程不结束运行的话,那么这些value就会一直存在,最后导致OOM。

虽然ThreadLocal中在进行set,get方法时底层都会调用expungeStaleEntry()来进行清理,但是在某些情况下依然会发生内存泄露,因此,一个良好的习惯依然是:当你不需要这个ThreadLocal变量时,主动调用remove(),这样对整个系统是有好处的

关于ThreadLocal的一些其他知识点

还有一些关于ThreadLocal的知识点也是需要我们了解的,比如ThreadLocalMap是通过开放寻址法来解决哈希冲突的;在ThreadLocal中还提供了一个可被继承的ThreadLocal------InheritableThreadLocal,支持父子线程间共享数据,有兴趣的朋友可以阅读以下文章,由于笔者实力有限,只能写到这里(后续可能会去研究一下)。

参考文章

ThreadLocal使用与原理 - 掘金 (juejin.cn)

Java面试必问:ThreadLocal终极篇 - 掘金 (juejin.cn)

相关推荐
你不是我我8 小时前
【Java 开发日记】HTTP3 性能更好,为什么内网微服务依然多用 HTTP2?HTTP2 内网优势是什么?
java·开发语言·微服务
雪碧聊技术8 小时前
大模型爆火!Java后端如何抓住Agent全栈开发的风口
java·大模型·agent·全栈开发
逻辑驱动的ken9 小时前
Java高频面试场景题25
java·开发语言·深度学习·面试·职场和发展
AI人工智能+电脑小能手10 小时前
【大白话说Java面试题】【Java基础篇】第32题:Java的异常处理机制是什么
java·开发语言·后端·面试
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ11 小时前
通过java后端代码来实现给word内容补充格式文本内容控件,以及 设置控件的标记和标题
java·c#·word
無限進步D12 小时前
Java 面向对象高级 接口
java·开发语言
逸Y 仙X13 小时前
文章二十七:ElasticSearch ES查询模板(Search Template)高效复用实战
java·大数据·数据库·elasticsearch·搜索引擎·全文检索
二哈赛车手13 小时前
新人笔记---Spring AI的Advisor以及其底层机制讲解(涉及源码),包含一些遇见的Spring AI的Advisor缺陷问题的解决方案
java·人工智能·spring boot·笔记·spring
AC赳赳老秦13 小时前
接口测试自动化:用 OpenClaw 对接 Postman,实现批量回归测试、测试报告自动生成与推送
java·人工智能·python·算法·elasticsearch·deepseek·openclaw