深入理解ThreadLocal原理

以下内容首发于我的个人网站,来这里看更舒适:https://riun.xyz/work/9898775
在这里插入图片描述


ThreadLocal是一种用于实现线程局部变量的机制,它允许每个线程有自己独立的变量,从而达到了线程数据隔离的目的。

基于JDK8

使用

通常在项目中是这样使用它的,创建线程变量工具类,然后用它去存取线程局部变量:

package com.example.demo.Utils;/*** @author: HanXu* on 2021/11/17* Class description: 线程变量工具类* 在每一个线程中储存一个变量,以记录当前线程生命流程中的动作*/
public class ThreadLocalUtil {private static final ThreadLocal<String> currentThreadLocal = ThreadLocal.withInitial(() -> new String());/*** 获取值* @return 当前线程中存放的变量值*/public static String getCurrentThreadVal() {return currentThreadLocal.get();}/*** set值* @param value 唯一*/public static void putCurrentThreadVal(String value) {currentThreadLocal.set(value);}/*** 清空当前线程中的数据*/public static void clear() {currentThreadLocal.remove();}
}

有了这个工具类,我们就能在线程执行时为每个线程放入不同的数据了,以下是一个小测试:

package com.example.demo.Utils;import java.util.Random;
import java.util.concurrent.*;/*** @author: HanXu* on 2024/6/26* Class description: 向10个线程中放入随机数,然后取出,查看放入取出是否一致*/
public class Test {private static final int THREAD_NUM = 10;private static final CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM);private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(THREAD_NUM);public static void main(String[] args) throws InterruptedException {ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_NUM);for (int i = 0; i < THREAD_NUM; i++) {threadPool.execute(() -> {Random t = new Random();int num = t.nextInt(100);//等待所有线程都有任务再全部一起执行:在当前线程中放入100以内的随机数waitOtherThread();ThreadLocalUtil.putCurrentThreadVal(String.valueOf(num));System.out.println(Thread.currentThread().getName() + ",放入的数字:" + num);countDownLatch.countDown();});}countDownLatch.await();System.out.println();System.out.println();for (int i = 0; i < THREAD_NUM; i++) {threadPool.execute(() -> {//等待所有线程都有任务再全部一起执行:取出当前线程中存放的数字waitOtherThread();System.out.println(Thread.currentThread().getName() + ",取得数字:" + ThreadLocalUtil.getCurrentThreadVal());ThreadLocalUtil.clear();});}threadPool.shutdown();while (!threadPool.awaitTermination(3, TimeUnit.SECONDS)) {Thread.yield();}System.out.println("执行完毕!");}private static void waitOtherThread() {try {cyclicBarrier.await();} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}}
}

执行结果:

pool-1-thread-10,放入的数字:80
pool-1-thread-8,放入的数字:22
pool-1-thread-7,放入的数字:52
pool-1-thread-6,放入的数字:99
pool-1-thread-5,放入的数字:91
pool-1-thread-3,放入的数字:26
pool-1-thread-4,放入的数字:25
pool-1-thread-1,放入的数字:64
pool-1-thread-2,放入的数字:56
pool-1-thread-9,放入的数字:38pool-1-thread-9,取得数字:38
pool-1-thread-10,取得数字:80
pool-1-thread-8,取得数字:22
pool-1-thread-7,取得数字:52
pool-1-thread-6,取得数字:99
pool-1-thread-5,取得数字:91
pool-1-thread-3,取得数字:26
pool-1-thread-4,取得数字:25
pool-1-thread-1,取得数字:64
pool-1-thread-2,取得数字:56
执行完毕!

原理

可以看到我们明明使用的是同一个ThreadLocal, 但作用到不同线程上就能隔离他们之间的数据。那ThreadLocal是如何做到线程间数据隔离的呢?

我们可以看下ThreadLocal.set的源码:

	public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}

可以看到通过getMap(t),得到了一个类似Map的对象ThreadLocalMap,然后向map中存入数据时,是以当前对象this为key存入的。当前对象this就是当前ThreadLocal对象,我们使用的是同一个ThreadLocal,所以this是一样的。

那就肯定是map不同,再看下getMap(t)怎么获取的:

	ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

Thread t是当前线程,t.threadLocals就是获取的当前线程的threadLocals属性。

(在Thread类里有一个成员属性:ThreadLocal.ThreadLocalMap threadLocals = null;)

那它的原理就是每个Thread内部有一个ThreadLocalMap类型的属性变量threadLocals。然后每个线程执行ThreadLocal.set时,是向自己的threadLocals中储存数据。由于线程不同,所以threadLocals也就不同,达到了线程数据隔离的目的。而ThreadLocal只是用来操作当前线程中的ThreadLocalMap的工具类而已。所有的数据并没有放在ThreadLocal当中。

所以一句话总结就是:每个线程有自己的ThreadLocalMap,存取数据是从自己的ThreadLocalMap操作的。

细节

ThreadLocalMap

ThreadLocalMap是一个Map,所以它也是数组+链表结构;但是由于我们使用的时候是一个ThreadLocal对象,而存数据时是以当前ThreadLocal对象作为key的,所以这个Map中只会有一个索引位置被使用,且不会有链表形成。(所以它内部并没有链表的实现)

Entry

我们再来看看Entry(ThreadLocal -> static class ThreadLocalMap -> static class Entry):

		static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}

很简单的k,v键值对构成的对象,但是它却继承了弱引用WeakReference,让自己的k变成了弱引用类型:super(k); 为什么是弱引用的key我们后文再说。

remove

一般我们使用set存值,get取值,当这个变量不再使用了,我们需要手动remote()清除掉,ThreadLocal.remote():

	public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}

ThreadLocal.ThreadLocalMap.remote():

		private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;//获取ThreadLocal作为key在数组中的索引下标int i = key.threadLocalHashCode & (len-1);//虽然是循环,但是我们的使用方式数组中只会有一个索引有值for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {//使用的父类的clear方法,即:把key置为nulle.clear();//把value置为null,通常也会把key为null的value置为nullexpungeStaleEntry(i);return;}}}

我们在项目中使用ThreadLocal,在当前线程用完后,一定要手动remove(),这样在当前线程生命周期结束的时候,所有对象都会变为垃圾可回收。

内存泄露

如果我们在使用完ThreadLocal后,不手动remove(),若当前线程生命周期还未结束,那线程会一直持有ThreadLocalMap的引用,而ThreadLocalMap引用Entry,Entry引用了对应的key,value,而我们使用完了ThreadLocal,ThreadLocal已经没有了,就永远无法再获取这个Entry的key,value,这样就会造成了内存泄露:

所以正确的使用方法是:定义一个ThreadLocal,在线程运行时使用它,在使用完成之后执行remove()。

另外我们项目中一般都是用static final修饰ThreadLocal也是因为我们在项目运行期间只想要一个ThreadLocal对象,这样当发生一些不可预料的事情时,由于我们只有一个ThreadLocal对象,所以我们也能够操作之前这个位置的Entry内容,修改或删除它。

Why WeakReference?

通过上图我们可以看到,无论Entry的key使用强引用还是弱引用,如果没有remove(),那在线程生命周期没有结束时,都是会造成内存泄露的。那为什么要使用弱引用呢?

因为ThreadLocal的set / get /remove方法执行时,都会做一些额外的事:将key为null的Entry里的value也置为null:

    public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)//在这个方法内部map.set(this, value);elsecreateMap(t, value);}

这样,假设我们在某个方法中定义了ThreadLocal,使用完未remove();而当我们下次在另一个方法中再定义一个ThreadLocal时,进行set / get /remove任意操作,都会将当前线程内ThreadLocalMap中key为null的Entry进行清除,将其value也置为null,将一个本该内存泄露的对象变为了可回收的垃圾。 这样相当于多了一层保障,从而减少内存泄露发生的可能:

可能有些同学注意到我上面强调了在线程生命周期内,这是因为如果线程生命周期结束了,则不管有没有remove(),对应的Entry一定会被作为垃圾回收。因为线程生命周期结束后,ThreadLocal和ThreadLocalMap都没有了,没有栈引用指向堆空间这些对象,所以他们都是垃圾可以被回收。

而在我们进行系统开发时,这点通常是不好控制的,因为有可能许多请求并发访问时,请求的线程都没有执行结束,所以如果我们不remove(),那一点点内存泄露就有可能导致内存溢出。

综上,ThreadLocal导致内存泄露的两个原因就是:

1、使用完没有remove()

2、使用完ThreadLocal后,线程生命周期并没有结束

所以要解决ThreadLocal的内存泄露问题,只需要满足任意一个即可:

1、一定要remove() 2、使用完ThreadLocal后,线程结束

但是第2点我们不好控制,所以一般都是使用第1点。

丢失ThreadLocal?

还有人可能在乎的点是:把key作为弱引用,发现即回收,若GC执行时发现了这个ThreadLocal那它不就被回收了吗?那我们程序执行不就出现NPE了吗?

其实不是的,因为我们还有一个自己定义的ThreadLocal threadLocal = new ThreadLocal()这个强引用在指向该ThreadLocal,所以在使用期间这个ThreadLocal是不会被垃圾回收的。

框架中的应用

Spring的事务管理器使用的ThreadLocal,SpringMVC的HttpSession、HttpServletRequest、HttpServletResponse都是放在ThreadLocal中的,以保证线程安全。

总结

因此,当我们想要隔离线程变量时,可以使用ThreadLocal,但是使用时要注意,一般定义一个static final的ThreadLocal,且使用完之后要一定记得remove();

另外ThreadLocal还有很多变种,比如InheritableThreadLocal和TransmittableThreadLocal。想要更多了解使用的可以看这篇文章:全链路追踪traceId

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JAVA连接FastGPT实现流式请求SSE效果

FastGPT 是一个基于 LLM 大语言模型的知识库问答系统&#xff0c;提供开箱即用的数据处理、模型调用等能力。同时可以通过 Flow 可视化进行工作流编排&#xff0c;从而实现复杂的问答场景&#xff01; 一、先看效果 真正实流式请求&#xff0c;SSE效果&#xff0c;SSE解释&am…

全球首款商用,AI为视频自动配音配乐产品上线

近日&#xff0c;海外推出了一款名为Resona V2A的产品&#xff0c;这是全球首款商用视频转音频 (V2A) 技术产品。这项突破性技术利用AI&#xff0c;仅凭视频数据即可自动生成高质量、与上下文相关的音频&#xff0c;包括声音设计、音效、拟音和环境音&#xff0c;为电影制作人、…

Ubuntu20.04安装Prometheus监控系统

环境准备&#xff1a; 服务器名称内网IP公网IPPrometheus服务器192.168.0.23047.119.21.167Grafana服务器192.168.0.23147.119.22.8被监控服务器192.168.0.23247.119.22.82 更改主机名方便辨认 hostnamectl set-hostname prometheus hostnamectl set-hostname grafana hostn…

Springboot下使用Redis管道(pipeline)进行批量操作

之前有业务场景需要批量插入数据到Redis中&#xff0c;做的过程中也有一些感悟&#xff0c;因此记录下来&#xff0c;以防忘记。下面的内容会涉及到 分别使用for、管道处理批量操作&#xff0c;比较其所花费时间。 分别使用RedisCallback、SessionCallback进行Redis pipeline …

【BES2500x系列 -- RTX5操作系统】深入探索CMSIS-RTOS RTX -- 同步与通信篇 -- 消息队列和邮箱处理 --(四)

&#x1f48c; 所属专栏&#xff1a;【BES2500x系列】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#x1f49…

【Node-RED 4.0.2】4.0版本新增特性(官方版)

二、重要功能 *1.时间戳格式改进 过去&#xff0c;node-red 只提供了 最原始的 timestamp 的格式&#xff08;1970-01-01 ~ now&#xff09; 但是现在&#xff0c;额外增加了 2 种格式&#xff1a; ISO 8601 -A COMMON FORMAT&#xff08;YYYY-MM-DDTHH:mm:ss:sssZ&#xff…

Linux环境安装配置nginx服务流程

Linux环境的Centos、麒麟、统信操作系统安装配置nginx服务流程操作&#xff1a; 1、官网下载 下载地址 或者通过命令下载 wget http://nginx.org/download/nginx-1.20.2.tar.gz 2、上传到指定的服务器并解压 tar -zxvf nginx-1.20.1.tar.gzcd nginx-1.20.1 3、编译并安装到…

阿里Nacos下载、安装(保姆篇)

文章目录 Nacos下载版本选择Nacos安装Windows常见问题解决 更多相关内容可查看 Nacos下载 Nacos官方下载地址&#xff1a;https://github.com/alibaba/nacos/releases 码云拉取&#xff08;如果国外较慢或者拉取超时可以试一下国内地址&#xff09; //国外 git clone https:…

[数据集][目标检测]桥梁检测数据集VOC+YOLO格式1116张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;1116 标注数量(xml文件个数)&#xff1a;1116 标注数量(txt文件个数)&#xff1a;1116 标注…

【RabbitMQ实战】Springboot 整合RabbitMQ组件,多种编码示例,带你实践 看完这一篇就够了

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、对RabbitMQ管理界面深入了解1、在这个界面里面我们可以做些什么&#xff1f; 二、编码练习&#xff08;1&#xff09;使用direct exchange(直连型交换机)&a…

snowflake 不再是个数据仓库公司了

标题先上结论&#xff0c;为啥这么认为&#xff0c;且听接下来道来。 snowflake 非常成功&#xff0c;开创了云数仓先河&#xff0c;至今在数仓架构上也是相对比较先进的&#xff0c;国内一堆模仿的公司&#xff0c;传统上我们会认为 snowflake 肯定是一家数据仓库公司。不过最…

3D Gaussian Splatting代码中的forward和backward两个文件代码解读

3dgs代码前向传播部分 先来讨论一下glm&#xff0c;因为定义变量的时候用到了这个。 glm的解释 glm 是指 OpenGL Mathematics&#xff0c;这是一个针对图形编程的数学库。它的全称是 OpenGL Mathematics (GLM)&#xff0c;主要用于 OpenGL 的开发。这个库是基于 C 的模板库&…

什么是CC攻击,如何防止网站被CC攻击的方法

前言 “CC攻击的原理就是攻击者控制某些主机不停地发大量数据包给对方服务器造成服务器资源耗尽&#xff0c;一直到宕机崩溃。” 什么是CC攻击&#xff1f; CC攻击前身是一个名为Fatboy的攻击程序&#xff0c;而之所以后来人们会称之为CC&#xff0c;也叫HTTP-FLOOD&#xff…

【AI提升】如何使用大模型:本机离线和FastAPI服务调用

大模型本身提供的功能&#xff0c;类似于windows中的一个exe小工具&#xff0c;我们可以本机离线调用然后完成具体的功能&#xff0c;但是别的机器需要访问这个exe是不可行的。常见的做法就是用web容器封装起来&#xff0c;提供一个http接口&#xff0c;然后接口在后端调用这个…

lodash.js 工具库

lodash 是什么? Lodash是一个流行的JavaScript实用工具库,提供了许多高效、高兼容性的工具函数,能够方便地处理集合、字符串、数值、函数等多种数据类型,大大提高工作效率。 lodash官网 文档参见:Lodash Documentation lodash 在Vue中怎么使用? 1、首先安装 lodash np…

JDK动态代理-AOP编程

AOPTest.java&#xff0c;相当于main函数&#xff0c;经过代理工厂出来的Hello类对象就不一样了&#xff0c;这是Proxy.newProxyInstance返回的对象&#xff0c;会hello.addUser会替换为invoke函数&#xff0c;比如这里的hello.addUser("sun", "13434");会…

Python 作业题1 (猜数字)

题目 你要根据线索猜出一个三位数。游戏会根据你的猜测给出以下提示之一&#xff1a;如果你猜对一位数字但数字位置不对&#xff0c;则会提示“Pico”&#xff1b;如果你同时猜对了一位数字及其位置&#xff0c;则会提示“Fermi”&#xff1b;如果你猜测的数字及其位置都不对&…

无人机生态环境监测、图像处理与GIS数据分析综合实践技术应用

构建“天空地”一体化监测体系是新形势下生态、环境、水文、农业、林业、气象等资源环境领域的重大需求&#xff0c;无人机生态环境监测在一体化监测体系中扮演着极其重要的角色。通过无人机航空遥感技术可以实现对地表空间要素的立体观测&#xff0c;获取丰富多样的地理空间数…

QT+winodow 代码适配调试总结(二)

已经好多年了&#xff0c; linux环境下不同版本的QT程序开发和部署&#xff0c;突然需要适配window环境程序调试&#xff0c;一堆大坑&#xff0c;还真是一个艰巨的任务&#xff0c;可是kpi下的任务计划&#xff0c;开始吧&#xff01;&#xff01; 1、首先我们自定义的动态库…

vue3使用v-html实现文本关键词变色

首先看应用场景 这有一段文本内容&#xff0c;是项目的简介&#xff0c;想要实现将文本中的关键词进行变色处理 有如下关键词 实现思路 遍历文本内容&#xff0c;找到关键词&#xff0c;并使用某种方法更改其字体样式。经过搜寻资料决定采用v-html实现&#xff0c;但是v-h…