ThreadLocal详解
一、ThreadLocal简介
1、简介
ThreadLocal叫做线程变量,它是一个线程的本地变量,意味着这个变量是线程独有的,是不能与其他线程共享的。这样就可以避免资源竞争带来的多线程的问题。
即 ThreadLocal类用来提供线程内部的局部变量,不同的线程之间不会相互干扰。
ThreadLocal变量,即线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
注意:
- ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。
- ThreadLocal 变量通常被 private static修饰。当一个线程(Thread)结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
2、ThreadLocal与Synchronized区别
ThreadLocal和Synchonized都用于解决多线程并发访问同一个资源对象的时候,可能就会出现线程不安全的问题。
- ThreadLocal是与一个线程绑定的本地变量,也就意味着这个变量是线程独有的,是不能与其他线程共享的。这样就可以避免资源竞争带来的多线程的问题。
- 加锁方式(synchronized、Lock) 用于在多个线程间通信时能够获得数据共享。
但是,ThreadLocal 这种解决多线程安全问题的方式与加锁方式(synchronized、Lock) 是有本质的区别。
两者区别如下:
(1)资源管理方面
- Synchronized通过加锁的方式,让多个线程之间逐一访问共享资源。
- ThreadLocal是每个线程都有一个资源副本,是不需要加锁的。
(2)实现方式方面
锁是通过时间换空间
的做法。
Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。ThreadLocal是通过空间换时间
的做法。
ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
3、使用场景
根据使用场景的不同,我们可以选择不同的技术手段,关键还是要看你的应用场景对于资源的管理是需要多线程之间共享
还是单线程内部独享
。
- 多线程之间共享资源:使用加锁方式。
- 单线程内部独享:使用 ThreadLocal变量。
ThreadLocal 适用于如下两种场景:
- 1)每个线程需要有自己单独的实例。
- 2)实例需要在多个方法中共享,但不希望被多线程共享。
二、ThreadLocal使用
使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal类的常用方法:
- ThreadLocal threadLocal = new ThreadLocal();:创建ThreadLocal对象(即一个线程本地变量)。
- initialValue():返回此线程局部变量的当前线程的"初始值" 。
- set(T value):将此线程局部变量的当前线程副本中的值设置为value。。
- get():返回此线程局部变量的当前线程副本中的值 。
- remove():移除当前线程绑定的局部变量,该方法可以帮助JVM进行GC。
1、示例1
public class ThreadLocalUseDemo1 {private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();private static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();/*** 运行 count个线程,每个线程持有自己独有的 String类型编号*/public void startThreadArray(int count) {Thread[] runs = new Thread[count];for (int i = 0; i < runs.length; i++) {// 赋值编号idnew ThreadDemo1(i).start();}}/*** 线程类:*/public static class ThreadDemo1 extends Thread {/*** 编号id*/private int codeId;public ThreadDemo1(int codeId) {this.codeId = codeId;}@Overridepublic void run() {String threadName = Thread.currentThread().getName();threadLocal1.set("threadLocal1赋值,线程_" + codeId);if (codeId == 2) {//如果是线程2,设置 threadLocal2变量,值乘以5threadLocal2.set(codeId * 5);}System.out.println(threadName + " -》 " + threadLocal1.get());System.out.println(threadName + " -》 " + threadLocal2.get());// 使用完移除,help GCthreadLocal1.remove();threadLocal2.remove();}}public static void main(String[] args) {ThreadLocalUseDemo1 useDemo = new ThreadLocalUseDemo1();// 启动3个线程useDemo.startThreadArray(3);}}
从示例1中可以看到,每个线程分别获取了自己线程存放的变量,他们之间变量的获取并不会错乱。
2、示例2
public class ThreadLocalUseDemo2 {//public static ThreadLocalUseDemo2.Number number = new ThreadLocalUseDemo2.Number(0);/*** 初始化 num值。使用时,先通过get方法获取。*/public static ThreadLocal<ThreadLocalUseDemo2.Number> threadLocalValue = new ThreadLocal<ThreadLocalUseDemo2.Number>() {@Overrideprotected Number initialValue() {return new Number(0);}};/*** 数据类*/private static class Number {public Number(int num) {this.num = num;}private int num;public int getNum() {return num;}public void setNum(int num) {this.num = num;}@Overridepublic String toString() {return "Number [num=" + num + "]";}}/*** 线程类:*/public static class ThreadDemo2 extends Thread {@Overridepublic void run() {// 如果没有初始化,注意NPE。// static修饰的 number时,注释掉这句Number number = threadLocalValue.get();//每个线程计数加随机数Random r = new Random();number.setNum(number.getNum() + r.nextInt(100));//将其存储到ThreadLocal中threadLocalValue.set(number);try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}//打印保存的随机值System.out.println(Thread.currentThread().getName() + " -》 " + threadLocalValue.get().getNum());threadLocalValue.remove();System.out.println(Thread.currentThread().getName() + " remove方法之后 -》 " + threadLocalValue.get().getNum());}}public static void main(String[] args) {// 启动5个线程for (int i = 0; i < 5; i++) {new ThreadDemo2().start();}}
}
从示例2中可以看到,每个线程可以通过 initialValue方法初始化变量值 。如果使用 public static ThreadLocalUseDemo2.Number number赋值,会导致数值一样。因为是 static修饰的,所有线程都指向同一个对象。
三、ThreadLocal源码分析
查看 ThreadLocal类的常用方法源码。
1、set()方法
查看 set方法。
public void set(T value) {// 1、获取当前线程Thread t = Thread.currentThread();// 2、获取线程中的属性 threadLocalMap//如果threadLocalMap 不为空,则直接更新要保存的变量值,否则创建threadLocalMap,并赋值ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);else// 初始化thradLocalMap 并赋值createMap(t, value);}
1.1 ThreadLocalMap简介
ThreadLocalMap是 ThreadLocal的内部静态类,其实 ThreadLocalMap是个标准的 Map实现,内部有一个元素类型为 Entry 的数组,用以存放线程可能需要的多个副本变量。
即 ThreadLocalMap的构成主要是用 Entry来保存数据 ,而且还是继承的弱引用。在 Entry内部使用 ThreadLocal作为key,使用我们设置的value作为value。
查看 ThreadLocalMap类。
static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}private void set(ThreadLocal<?> key, Object value) {// We don't use a fast path as with get() because it is at// least as common to use set() to create new entries as// it is to replace existing ones, in which case, a fast// path would fail more often than not.Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;// hash冲突时,使用了开放定址法(线性探测再散列即依次向后查找)。e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}// ...}
可以看到 Entry 内部静态类,它继承了 WeakReference(弱引用),一个key是 ThreadLocal<?>类型,一个value是 Object 类型的值。
- getEntry 方法:是获取某个 ThreadLocal 对应的值。
- set 方法:是更新或赋值相应的 ThreadLocal对应的值。
1.2 初始化ThreadLocalMap
接着查看 ThreadLocal.createMap方法。
void createMap(Thread t, T firstValue) {// 创建 ThreadLocalMapt.threadLocals = new ThreadLocalMap(this, firstValue);}//ThreadLocalMap 构造方法ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}
注意:t.threadLocals 的代表的字段信息。
通过这里我们发现,每个线程都拥有一个 ThreadLocalMap类用 Entry存放保存数据,在 Entry内部使用 ThreadLocal作为key,使用我们设置的value作为value。从而保证每个线程内部的局部变量副本相互干扰。
2、get方法
查看 get方法。
public T get() {//1、获取当前线程Thread t = Thread.currentThread();//2、获取当前线程的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {//3.1、获取threalLocalMap中存储的值ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//3.2 如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为nullreturn setInitialValue();}private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}
get 方法,其实就是拿到每个线程独有的 ThreadLocalMap。
然后再用 ThreadLocal 的当前实例,拿到 Map 中的相应的 Entry,然后就可以拿到相应的值返回出去。如果 Map 为空,还会先进行 map 的创建,初始化等工作并返回null。
3、remove方法
查看 remove方法。
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}/*** Remove the entry for key.*/private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();expungeStaleEntry(i);return;}}}
remove方法,直接将 ThrealLocal 对应的值从当前相差 Thread中的 ThreadLocalMap中删除。
如果使用完不删除的话,就会涉及到内存泄露的问题。
这是因为 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用。
弱引用的特点是:如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。因为,一般定义ThreadLocal 变量通常被 private static修饰。
参考文章:
- ThreadLocal源码解析:https://blog.csdn.net/qq_26470817/article/details/124993311
– 求知若饥,虚心若愚。
相关文章:

ThreadLocal详解
一、ThreadLocal简介 1、简介 ThreadLocal叫做线程变量,它是一个线程的本地变量,意味着这个变量是线程独有的,是不能与其他线程共享的。这样就可以避免资源竞争带来的多线程的问题。 即 ThreadLocal类用来提供线程内部的局部变量࿰…...

利用Cookie劫持+HTML注入进行钓鱼攻击
目录 HTML注入和cookie劫持: 发现漏洞 实际利用 来源 HTML注入和cookie劫持: HTML注入漏洞一般是由于在用户能够控制的输入点上,由于缺乏安全过滤,导致攻击者能将任意HTML代码注入网页。此类漏洞可能会引起许多后续攻击&#…...
【接口汇总】常用免费的API
短信API 短信验证码:可用于登录、注册、找回密码、支付认证等等应用场景。支持三大运营商,3秒可达,99.99%到达率,支持大容量高并发。 通知短信:当您需要快速通知用户时,通知短信是最快捷有效的…...
数字信号处理知识点
数字信号处理知识点1 频谱图中,横坐标取值范围的含义2 MATLAB常用函数2.1 波形产生2.2 滤波器分析2.3 滤波器实现2.4 线性系统变换2.5 滤波器设计2.5.1 FIR滤波器2.5.2 IIR滤波器2.6 Transforms(变换)2.7 统计信号处理和谱分析2.8 Windows(窗函数)2.9 Parametric Mo…...

计算机网络第八版——第三章课后题答案(超详细)
第三章 该答案为博主在网络上整理,排版不易,希望大家多多点赞支持。后续将会持续更新(可以给博主点个关注~ 第一章 答案 第二章 答案 【3-01】数据链路(即逻辑链路)与链路(即物理链路)有何区…...

九龙证券|磷酸亚铁锂是什么?磷酸亚铁锂的特点和性能介绍
磷酸亚铁锂是一种新式锂离子电池电极资料,化学式:LiFePO4,磷酸亚铁锂为近来新开发的锂离子电池电极资料,首要用于动力锂离子电池,作为正极活性物质运用,人们习气也称其为磷酸铁锂。 磷酸亚铁锂的特色和功能…...

3D目标检测(二)—— 直接处理点云的3D目标检测网络VoteNet、H3DNet
前言上次介绍了基于Point-Based方法处理点云的模块,3D目标检测(一)—— 基于Point-Based方法的PointNet点云处理系列,其中相关的模块则是构成本次要介绍的,直接在点云的基础上进行3D目标检测网络的基础。VoteNet对于直接在点云上预…...
Java学习-IO流-常用工具包(hutool)
Java学习-IO流-常用工具包(hutool) hutool工具包 DateUtil:日期时间工具类 TImeInterval:计时器工具类 StrUtil:字符串工具类 HexUtil:16进制工具类 HashUtil:Hash算法类 ObjectUtil࿱…...

【LeetCode】1. 两数之和
题目链接:https://leetcode.cn/problems/two-sum/ 📕题目要求: 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 你可以假设每种输入…...

【数值模型环境搭建】Intel编译器安装
Intel编译器在数值模型编译中被广泛使用,它有一个很好的地方是自带Mpich,不需要额外安装。本文介绍Intel2018.1.163版本的安装。 1、安装包获取 Intel编译器可从官网下载下载: https://www.intel.cn/content/www/cn/zh/homepage.html 或者…...
操作VMware vCenter Converter 实现物理机迁移到虚拟机
实验目的:熟练VMware虚拟化项目中,物理机向ESXI5迁移操作过程。 1、打开VMwarevCenterConverterStandalone5.0软件,按“转换计算机”。 2、选择“已打开电源的计算机”。并输入远程要连接迁移物理机IP地址,登录帐户和密码。 然后…...

hutool XML反序列化漏洞(CVE-2023-24162)
漏洞简介 Hutool 中的XmlUtil.readObjectFromXml方法直接封装调用XMLDecoder.readObject解析xml数据,当使用 readObjectFromXml 去处理恶意的 XML 字符串时会造成任意代码执行。 漏洞复现 我们在 maven 仓库中查找 Hutool https://mvnrepository.com/search?…...

Java简单认识泛型——图文详解
写在开头:想必大家和博主一样,在以往学习JavaSE的语法中,遇到了一个陌生的词——泛型,博主当时很好奇,什么是泛型呢?即使是学完了JavaSE,这个问题都没有解决,只能在百度查阅了解关于泛型的一些皮…...
AcWing171.送礼物
题目描述 达达帮翰翰给女生送礼物,翰翰一共准备了NNN 个礼物,其中第 iii 个礼物的重量是 G[i]G[i]G[i]。 达达的力气很大,他一次可以搬动重量之和不超过 WWW 的任意多个物品。 达达希望一次搬掉尽量重的一些物品,请你告诉达达在…...

领域驱动设计-架构篇
目录 1、软件架构概述 1.1 软件架构概念 1.2 软件架构分类 1.3 软件架构模式 1.4 软件架构风格 2、领域驱动软件架构 2.1 架构风格 六边行架构(领域驱动设计首选) 为什么选择REST架构 松耦合 可伸缩性 易用性 约束性 2.2 架构模型 命令和…...
docker安装kafka
前言最近在用kafka做项目,所以本地搭建下kafka,但是又嫌java安装和安装kafka太麻烦,所以想到用docker来部署。镜像wurstmeister/kafka维护较为频繁的一个Kafka镜像。只包含了Kafka,因此需要另行提供ZooKeeper,推荐使用…...
Selenium4+Python3系列(十一) - Page Factory设计模式
写在前面: Page Object模式,目的是将元素定位和元素操作分层,只接触测试内容,不写基础内容,便于后续对自动化测试用例体系的维护,这是中心思想,也是核心。 那么我们继续将简洁延续,…...
C++基础知识【4】函数及参数
目录 一、函数的基本概念 1.1、构成 1.2、声明和定义 1.3、函数的调用 二、参数 2.1、形参和实参 2.2、参数的传递 传值 传引用 传指针 三、C函数的一些新特性 3.1、Lambda表达式 3.2、右值引用 3.3、默认参数 3.4、变长参数模板 3.5、constexpr函数 3.6、noex…...
约瑟夫森磁效应
电流与波函数的相位有直接的关系,可得约瑟夫森结的电流为 IIcsinϕ\begin{align} II_c sin\phi \end{align} IIcsinϕ 式中,IcI_cIc为临界电流,相位差为ϕϕ2−ϕ1\phi\phi_2-\phi_1ϕϕ2−ϕ1。 根据磁矢势A的定义,B…...

什么是L1和L2正则化,以及它们有什么区别
一、L1和L2正则化是什么? 在防止过拟合的方法中有L1正则化和L2正则化,L1和L2是正则化项,又叫做惩罚项,是为了限制模型的参数,防止模型过拟合而加在损失函数后面的一项。 在二维的情况下,黄色的部分是L2和…...
浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)
✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义(Task Definition&…...
Python爬虫实战:研究MechanicalSoup库相关技术
一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...
MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例
一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...
在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module
1、为什么要修改 CONNECT 报文? 多租户隔离:自动为接入设备追加租户前缀,后端按 ClientID 拆分队列。零代码鉴权:将入站用户名替换为 OAuth Access-Token,后端 Broker 统一校验。灰度发布:根据 IP/地理位写…...
ffmpeg(四):滤镜命令
FFmpeg 的滤镜命令是用于音视频处理中的强大工具,可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下: ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜: ffmpeg…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...

JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作
一、上下文切换 即使单核CPU也可以进行多线程执行代码,CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短,所以CPU会不断地切换线程执行,从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...

成都鼎讯硬核科技!雷达目标与干扰模拟器,以卓越性能制胜电磁频谱战
在现代战争中,电磁频谱已成为继陆、海、空、天之后的 “第五维战场”,雷达作为电磁频谱领域的关键装备,其干扰与抗干扰能力的较量,直接影响着战争的胜负走向。由成都鼎讯科技匠心打造的雷达目标与干扰模拟器,凭借数字射…...