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

相关推荐
昙鱼3 分钟前
springboot创建web项目
java·前端·spring boot·后端·spring·maven
eternal__day3 分钟前
数据结构(哈希表(中)纯概念版)
java·数据结构·算法·哈希算法·推荐算法
天之涯上上8 分钟前
JAVA开发 在 Spring Boot 中集成 Swagger
java·开发语言·spring boot
2402_857583499 分钟前
“协同过滤技术实战”:网上书城系统的设计与实现
java·开发语言·vue.js·科技·mfc
白宇横流学长10 分钟前
基于SpringBoot的停车场管理系统设计与实现【源码+文档+部署讲解】
java·spring boot·后端
APP 肖提莫13 分钟前
MyBatis-Plus分页拦截器,源码的重构(重构total总数的计算逻辑)
java·前端·算法
kirito学长-Java15 分钟前
springboot/ssm太原学院商铺管理系统Java代码编写web在线购物商城
java·spring boot·后端
爱学习的白杨树15 分钟前
MyBatis的一级、二级缓存
java·开发语言·spring
Code成立26 分钟前
《Java核心技术I》Swing的网格包布局
java·开发语言·swing
中草药z32 分钟前
【Spring】深入解析 Spring 原理:Bean 的多方面剖析(源码阅读)
java·数据库·spring boot·spring·bean·源码阅读