学习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)

相关推荐
可乐加.糖10 分钟前
腾讯云K8s容器部署SpringBoot项目实现方案
java·spring boot·容器·kubernetes·k8s·腾讯云
不断前进的皮卡丘21 分钟前
06-公寓租赁项目-后台管理-公寓管理篇
java·开发语言·数据库·spring boot
luoluoal24 分钟前
Java项目之基于ssm的个性化旅游攻略定制系统(源码+文档)
java·mysql·mybatis·ssm·源码
永无魇足30 分钟前
JAVASE(十五)正则表达式
java
天狼122233 分钟前
java 正则表达式优化
java·mysql·正则表达式
顾林海1 小时前
深度解析LinkedList工作原理
android·java·面试
茶本无香1 小时前
CompletableFuture:整合、超时、完成事件与批量处理
java·超时·future·completable·completion·任务整合
雷渊1 小时前
分析@Autowired和@Resource的使用场景
java·后端·面试
whisperrr.1 小时前
【spring02】Spring 管理 Bean-IOC,基于 XML 配置 bean
xml·java·spring
Cloud_.1 小时前
Spring Boot整合Elasticsearch
java·spring boot·后端·elasticsearch·es