深入理解ThreadLocal看这篇就够了-应用场景、内部原理、内存泄漏以及父子线程如何共享数据

news2025/1/11 6:30:10

为了帮助大家在项目中更好使用ThreadLocal,本文向大家介绍ThreadLocal原理和常见问题,具体内容如下:

  1. ThreadLocal是什么

  1. ThreadLocal的应用场景

  1. ThreadLocal的内部原理

  1. ThreadLocal内存泄露问题

  1. 父子线程如何共享数据


ThreadLocal是什么

java.lang.ThreadLocal是JDK中提供的一个类,在并发编程中,为解决线程安全问题提供了一种用空间换时间的新思路。

在一些高并发的场景中,如果需要对公共资源进行操作,我们第一时间就会想到使用synchronized或Lock,给访问公共资源的代码上锁,来保证了代码的原子性。但是多个线程同时竞争同一把锁的时候,可能会造成大量的锁等待,可能会浪费很多时间,让系统的响应时间变慢。这个时候我们就可以考虑是否可以使用ThreadLocal。

将类变量放到ThreadLocal类型的对象中,就可以使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。


ThreadLocal的应用场景

1、全局存储用户信息

当用户登陆后,会将用户信息存入token中并返回给前端,当用户调用需要授权的接口时,前端会把token放到header中去请求后端接口,后端在拦截器中解析token,获取用户信息,然后存放到某个工具类的ThreadLocal变量中,后续执行代码过程中,就不需要关注如何获取用户信息,只需要使用工具类get方法就可以获取。

2、SimpleDateFormat与ThreadLocal结合使用

大家都知道SimpleDateFormat是不安全类,但是做一些日期处理的时候又经常会用到这个类,这个时候我们可以与ThreadLocal结合进行使用,从而避免了线程安全问题。

// 这样来申明全局变量

public final static ThreadLocal<SimpleDateFormat> DF = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

3、org.slf4j.MDC处理链路追踪ID

MDC类本身包含了一个MDCAdapter对象,此对象里又包含了一个ThreadLocal的全局变量,可以用来处理日志相关的信息。例如常见的我们处理traceId的时候,先在过滤器里生成traceId并放入MDC,然后在日志文件里配置traceId,这样我们打印日志的时候就可以看到traceId了。

@Override

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

// 此处生成traceId...

// 将traceId放入MDC

MDC.put("traceId", traceId);

try {

filterChain.doFilter(servletRequest, servletResponse);

} catch (Exception e) {

MDC.remove("traceId");

}

<!-- 控制台日志 -->

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">

<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">

<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %level %C.%M[%F:%L] traceId[%X{traceId:-default}] - %msg%n

</Pattern>

</encoder>

</appender>


ThreadLocal的内部原理

想要搞清楚ThreadLocal的底层实现原理,我们不得不扒一下jdk源码。先看一下ThreadLocal类,发现有set、get、remove三个关键的方法:

public void set(T value) {

//获取当前线程

Thread t = Thread.currentThread();

//获取当前线程的成员变量ThreadLocalMap对象

ThreadLocalMap map = getMap(t);

//如果map不为空

if (map != null)

//将值设置到map中,key是this,即threadLocal对象,value是传入的value值

map.set(this, value);

else

//如果map为空,则需要创建新的map对象

createMap(t, value);

}

public T get() {

//获取当前线程

Thread t = Thread.currentThread();

//获取当前线程的成员变量ThreadLocalMap对象

ThreadLocalMap map = getMap(t);

if (map != null) {

//根据threadLocal对象从map中获取Entry对象

ThreadLocalMap.Entry e = map.getEntry(this);

if (e != null) {

@SuppressWarnings("unchecked")

//获取保存的数据

T result = (T)e.value;

return result;

}

}

//初始化数据

return setInitialValue();

}

public void remove() {

//获取当前线程的ThreadLocalMap对象

ThreadLocalMap m = getMap(Thread.currentThread());

if (m != null)

//如果ThreadLocalMap对象不为空,则删除,key是this,即threadLocal对象

m.remove(this);

}

从以上源码中,可以看到set、get、remove方法实际上都是在操作ThreadLocalMap对象中的数据,而ThreadLocalMap对象又是从当前线程中获取到的,所以不同的线程之间,ThreadLocalMap对象中的数据是隔离开的。


ThreadLocal内存泄露问题

首先一句话简单概括什么是内存泄露:由于某种原因,导致程序中的某些资源(内存)无法得到释放,因此造成了系统内存泄露。

在真正解释内存泄露之前我们还得简单回顾一下JAVA中的四种引用类型的区别:

1、强引用:发生了GC,对象也不会被回收

2、软引用(SoftReference):发生了GC,如果内存足够,则对象不会被回收;反之,会被回收

3、弱引用(WeakReference):发生了GC,无论内存是否足够,对象都会被回收

4、虚引用(PhantomReference):所指向的对象获取不到、拿出来是null,因此也叫幽灵对象

先来看下ThreadLocalMap的结构

static class ThreadLocalMap {

//此处省略...

//可以看出Entry继承了弱引用

static class Entry extends WeakReference<ThreadLocal<?>> {

/** The value associated with this ThreadLocal. */

Object value;

Entry(ThreadLocal<?> k, Object v) {

super(k);

value = v;

}

}

//此处省略...

}

那么问题来了,ThreadLocalMap的Entry为什么使用了弱引用,跟内存泄露又有什么关系呢?

我们分别来看假设key是强引用原本的弱引用情况下怎么样会发生内存泄漏

从上面的对比,我们可以看到,ThreadLocal 内存泄漏的根源是:由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除(remove()方法)对应 key 就会导致内存泄漏。要避免内存泄漏有两种方式:

1、使用完ThreadLocal,调用其remove方法删除对应的Entry

2、使用完ThreadLocal,当前Thread也随之运行结束

第一种方式很好理解。但是第二种方式显然就不好控制,特别是使用线程池的时候,线程执行代码结束后是不会销毁的。也就是说,只要记得在使用完ThreadLocal后及时的调用 remove,无论 key 是强引用还是弱引用都不会有问题。

那么为什么key要用弱引用呢?事实上,在ThreadLocalMap中,只要调用了它的get、set或remove三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空,如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了,这样就能最大程度(就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收.对应value在下一次ThreadLocaIMap调用 set/get/remove中的任一方法的时候会被清除,从而避免内存泄漏)的解决内存泄露问题。

// 这里是清除过期的entry的核心方法

private int expungeStaleEntry(int staleSlot) {

//此处省略...

// Rehash until we encounter null

Entry e;

int i;

for (i = nextIndex(staleSlot, len);

(e = tab[i]) != null;

i = nextIndex(i, len)) {

ThreadLocal<?> k = e.get();

if (k == null) {

// 当k为null时,把value也改为null

e.value = null;

tab[i] = null;

size--;

} else {

//此处省略...

}

}

return i;

}


父子线程如何共享数据

前面介绍的ThreadLocal都是在一个线程中保存和获取数据的。但在实际工作中,有可能是在父子线程中共享数据的。即在父线程中往ThreadLocal设置了值,在子线程中能够获取到。

public class ThreadLocalTest {

public static void main(String[] args) {

ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

threadLocal.set(6);

System.out.println("父线程获取数据:" + threadLocal.get());

new Thread(() -> {

System.out.println("子线程获取数据:" + threadLocal.get());

}).start();

}

}

执行结果:

父线程获取数据:6

子线程获取数据:null

你会发现,在这种情况下使用ThreadLocal是行不通的。main方法是在主线程中执行的,相当于父线程。在main方法中开启了另外一个线程,相当于子线程。显然通过ThreadLocal,无法在父子线程中共享数据。那么这个时候可以使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。

修改代码之后:

public class ThreadLocalTest {

public static void main(String[] args) {

InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();

threadLocal.set(6);

System.out.println("父线程获取数据:" + threadLocal.get());

new Thread(() -> {

System.out.println("子线程获取数据:" + threadLocal.get());

}).start();

}

}

执行结果:

父线程获取数据:6

子线程获取数据:6

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/194037.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CCS10新建TMS320F28335工程

CCS10新建TMS320F28335工程 1. 新建工程 点击Project → New CCS Project。选择芯片类型(TMS320F28335)、仿真器类型(XDS200V3)、新建工程名称、选择新建一个空工程。 2. 配置工程选项 右键项目工程名&#xff0c;进入配置选项Proprrties。或者AltEnter打开配置选项。在工程…

gd32f103vbt6 串口OTA升级-问题记录-2-平衡OTA弊端

走在路上的时候&#xff0c;我想起了这个OTA的弊端&#xff0c;那我想有没有办法解决呢&#xff1f;其实是有的。 那就是我还是把app程序放在flash的最开始的位置&#xff0c;而把OTA的程序放到后面&#xff08;flash的最后12k&#xff09;去。 这样也带来新的弊端&#xff1…

PMP考试技巧PMP考试大纲

一&#xff0c;PMP解题策略 PMP考试默认条件 精准审题 E(Eye):找到题眼&#xff1b; K(Key):找到考点&#xff1b; C(Choice):确定 选项&#xff1b; 情景结合 基本知识从PMI的角度出发&#xff0c;按PMI的思维方式&#xff0c;项目经理应该做出什么决定&#xff1b; 选…

SpringBoot自定义全局异常处理

自定义全局异常处理一. 创建所需类1. 自定义异常接口2. 自定义枚举类3. 自定义异常类4. 自定义异常处理类5. 自定义全局响应类5.1 BaseResponse类5.2 RespGenerator类二. 效果演示我们在 SpringBoot 项目中&#xff0c;往往会写许多 Controler 接口类&#xff0c;由于 Controll…

SNV的使用

一&#xff1a;什么是SVN&#xff1f; SVN是一个版本控制系统&#xff0c;SVN全称Subversion&#xff0c;用于记录一个或多个文件内容变化&#xff0c;方便我们查阅特定版本的修改情况。以前在没有版本控制的时候&#xff0c;我们通常在项目根目录下这样命名项目&#xff1a;p…

Python 环境搭建配置

Python可应用于多平台包括 Linux 和 Mac OS X。你可以通过终端窗口输入 "python" 命令来查看本地是否已经安装Python以及Python的安装版本。Unix (Solaris, Linux, FreeBSD, AIX, HP/UX, SunOS, IRIX, 等等。)Win 9x/NT/2000Macintosh (Intel, PPC, 68K)OS/2DOS (多个…

微信小程序 Springboot校园达达互助平台快递代取系统 java

本 录 摘 要 III Abstract 1 1 系统概述 1 1.1 概述 2 1.2课题意义 3 1.3 主要内容 4 2 系统开发环境 5 2.1微信开发者工具 6 2.2小程序框架以及目录结构介绍 6 2.3 JAVA简介 7 2.4 MySQL数据库 7 3 需求分析 8 3.1 系统设计目标 8 3…

STM32开发(1)----stm32f103c6t6开发板介绍和环境搭建

stm32f103c6t6开发板介绍一、stm32f103c6t6芯片资源介绍STM32 的命名规则二、最小系统开发板介绍三、开发板基本使用方法软件安装MDK5 安装安装STM32芯片包安装licenseUSB转串口驱动安装四、本文小结一、stm32f103c6t6芯片资源介绍 stm32f103c6t6 是一款基于 ARM Cortex M3 内…

测试岗外包4年终上岸,这段日子说起来都是泪啊

昨天一个老哥找到我倾诉&#xff0c;他干了好几年外包&#xff0c;现在通过自己的努力应聘上了阿里测试开发&#xff0c;虽然只是P6&#xff0c;但也属实不容易了。这位老哥是湖南长沙毕业的&#xff0c;计算机专业&#xff0c;二流本科。长沙&#xff0c;湖南省会&#xff0c;…

Python图像合成与视频倒放

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录前言一、视频抓帧二、图像转换1&#xff0c;图像翻转2&#xff0c;图像文件倒序三、倒序视频合成四、图像截取拓展任务——动态图合成与倒放1&#xff0c;gif动态图生…

面了个阿里拿28k跳槽出来的,真正见识到了跳槽天花板

2022年已经结束了&#xff0c;迎来的是2023崭新的一年&#xff0c;最近内卷严重&#xff0c;各种跳槽裁员&#xff0c;相信很多小伙伴也在准备金三银四的面试计划。 作为一个入职5年的老人家&#xff0c;目前工资比较乐观&#xff0c;但是我还是会选择跳槽&#xff0c;因为感觉…

MySQL表中的联合查询

上一篇有关聚合查询的博客:MySQL表中的聚合查询_徐憨憨&#xff01;的博客-CSDN博客主要是针对单个表进行查询操作,但是实际的开发环境中,数据往往来自己于不同的数据表,此时就需要使用联合查询进行操作!联合查询多表查询的基本执行过程:进行笛卡尔积然后设置条件删除无效数据进…

Echarts环形图线性渐变,hover后显示阴影

第004个点击查看专栏目录Echarts的渐变在上一篇文章中已经讲过 ECharts线性渐变色示例演示&#xff08;2种渐变方式&#xff09;&#xff0c;这里做了环形图&#xff0c;饼图的一个渐变示例演示&#xff0c;hover到元素后&#xff0c;会呈现出一个阴影。注意&#xff0c;颜色属…

年度总结 | 积跬步以至千里,2023一起筑梦新征程

2022年&#xff0c;是值得载入史册的一年。疫情开放&#xff0c;健康码隐入历史尘埃。国际形势紧张&#xff0c;信创化进入快车道。企业加速转型&#xff0c;跨界技术融合的运维新生态已初露苗头。回顾2022&#xff0c;我们聚沙成塔逆寒流而勇进&#xff0c;精造创新以实践诠释…

【Linux】基础网络编程

计算机网络基本概念 在网络操作系统&#xff0c;网络管理软件及网络通信协议的管理和协调下&#xff0c;实现资源共享和信息传递的计算机系统。 计算机网络的分类与一般的事物分类方法一样&#xff0c;可以按事物所具有的不同性质特点分类。计算机网络通俗地讲就是由多台计算…

VMware双网卡配置(ubuntu)

桥接的时候不能上网&#xff0c;上网的时候又不能桥接和开发板通信&#xff0c;这是一个非常难受的事情&#xff0c;下面我来配置一下双网卡&#xff0c;一个用来桥接和单片机通信&#xff0c;一个用来上网。 ⚫ NAT 网卡&#xff1a;Ubuntu 通过它上网&#xff0c;只要 Windo…

【快速开始】vuejs环境搭建第一个项目

本篇包含vuejs环境安装以及通过vue客户端快速创建运行第一个项目。(注&#xff1a;以下内容均已windows平台为基准) 目录&#xff1a; 一、安装nodejs 二、配置国内源加速 三、安装vue客户端 四、创建第一个应用 1、安装nodejs&#xff1a; 1.1、下载 官网下载地址&…

【Java】java | smart-doc + tonar | API文档统一管理

一、说明 1、准备舍弃swagger了&#xff0c;拥抱smart-doc 2、win10 3、jdk8 4、idea 二、搭建torna 1&#xff09;下载zip https://foruda.gitee.com/attach_file/1672544760054905357/torna-1.19.4.zip?token25e02a8e2817a757a0aa47172349cc20&ts1675345098&am…

06 Sentinel规则持久化(3)

Sentinel 持久化模式 Sentinel规则的推送有下面三种模式: 1、原始模式 如果不做任何修改&#xff0c;Dashboard 的推送规则方式是通过 API 将规则推送至客户端并直接更新到内存中&#xff1a; 这种做法的好处是简单&#xff0c;无依赖&#xff1b;坏处是应用重启规则就会消失…

智能网联汽车信息安全敲响「警钟」,如何筑牢防线?

2009年起&#xff0c;谷歌、百度等互联网科技公司就入局自动驾驶&#xff0c;旨在赋予汽车更智慧的大脑。 如今&#xff0c;物流、港口等场景下自动驾驶的商业化落地&#xff0c;众多的网联化接口不仅加强了智能网联汽车与操作环境之间的紧密关联&#xff0c;也促使智能网联汽…