企业运营能力指标/百度视频排名优化
1. ThreadLocal 是什么
JDK 对ThreadLocal
的描述为:
此类提供线程局部变量。这些变量与普通变量的不同之处在于,每个访问一个变量的线程(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,这些字段希望将状态与线程(例如,用户ID或事务ID)相关联。
说白了,ThreadLocal
就是用来存放线程自身相关数据的一个容器,这个容器叫做ThreadLocalMap
,它是ThreadLocal
的一个静态内部类,同时作为Thread
类的一个成员变量。ThreadLocal
在使用时,先拿到当前线程的成员变量ThreadLocalMap
,以当前的ThreadLocal
对象作为key
,变量作为value
存入ThreadLocalMap
。 然后每个线程取变量都是从线程各自的ThreadLocalMap
中取值,自然是线程安全的了。因为变量只在自己线程的生命周期内起作用,所以说ThreadLocal
提供线程局部变量,或者叫线程本地变量。
ThreadLocal 的特点有3个:
- 线程并发:在多线程并发的场景下使用。
- 数据传递:通过 ThreadLocal ,在同一个线程中,不同组件中传递公共变量。
- 线程隔离:不同线程之间互不干扰,这种变量在线程的生命周期内起作用。
2. ThreadLocal 怎么用
ThreadLocal 的常用方法有:
public ThreadLocal()
:通过构造器创建对象。一般是静态的。<S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)
:初始化一个 ThreadLcoal。void set(T value)
:设置当前线程绑定的局部变量。T get()
:获取当前线程绑定的局部变量。void remove()
:删除当前线程绑定的局部变量。
2.1 使用入门
2.1.1 原始版本
现在模拟一个需求,一个线程在业务开始时初始化一个用户 id(类似在一次web请求中上下文中初始化一下用户信息),业务结束时获取这个用户 id(比如用来打印日志,或者作为一个公共变量运用到业务编码中),存在多个这样的线程。
public class ThreadLocalTest {private String userId;private String getUserId() {return userId;}private void setUserId(String userId) {this.userId = userId;}public static void main(String[] args) {ThreadLocalTest test = new ThreadLocalTest();for (int i = 1; i < 6; i++) {Thread thread = new Thread(() -> {// 当前线程初始化userIdtest.setUserId(Thread.currentThread().getName() + "的userId");// 执行其他业务代码System.out.println("===执行业务代码===");// 当前线程获取userIdSystem.out.println(Thread.currentThread().getName() + "-->" + test.getUserId());});thread.setName("线程" + i);thread.start();}}
}
一种可能的结果:
===执行业务代码===
线程2-->线程1的userId
===执行业务代码===
线程1-->线程3的userId
===执行业务代码===
线程3-->线程3的userId
===执行业务代码===
线程4-->线程4的userId
由于线程调度的不确定性,可能线程1运行到一半,切换到了线程2,于是线程2获取到的 userId 是线程1设置的。也就是说,每个线程之间的变量不是隔离的,造成数据错误。
2.1.2 ThreadLocal 版本
每个线程中的变量都存放到自己的线程当中,所以这些变量叫做线程局部变量很形象。
public class ThreadLocalTest {private static ThreadLocal<String> context = new ThreadLocal<>();private String getUserId() {return context.get();}private void setUserId(String userId) {context.set(userId);}public static void main(String[] args) {ThreadLocalTest test = new ThreadLocalTest();for (int i = 1; i < 5; i++) {Thread thread = new Thread(() -> {test.setUserId(Thread.currentThread().getName() + "的userId");System.out.println("===执行业务代码===");System.out.println(Thread.currentThread().getName() + "-->" + test.getUserId());context.remove(); // 使用完清理线程局部变量});thread.setName("线程" + i);thread.start();}}
}
这样每个线程就互不干扰,不会取错变量值。一种可能的结果如下:
===执行业务代码===
线程1-->线程1的userId
===执行业务代码===
线程4-->线程4的userId
===执行业务代码===
线程2-->线程2的userId
===执行业务代码===
线程3-->线程3的userId
2.1.3 synchronized 版本
如果只看结果的正确性,用 synchronized 给业务代码块加锁也是可以完成的。如下:
Thread thread = new Thread(() -> {synchronized (ThreadLocalTest.class) {test.setUserId(Thread.currentThread().getName() + "的userId");System.out.println("===执行业务代码===");System.out.println(Thread.currentThread().getName() + "->" + test.getUserId());}
});
这样完全可以实现需求,但是 synchronized 的问题是什么呢?我们总说谁谁谁是线程安全的类,因为它有 synchronized 修饰。就是因为 synchronized 让多线程变成了单线程,它一次只允许一个线程执行,它能不安全吗?但它带来的代价是性能的下降,它不能并发执行,而 ThreadLocal 可以并发执行。
2.1.4 ThreadLocal 和 synchronized 对比
综上,synchronized 和 ThreadLocal 两个处理问题的角度和场景是不同的。
- synchronized 的侧重点在于保证操作的原子性,保证并发场景下共享变量的数据一致性。
- ThreadLocal 强调线程隔离性,不同的线程互不干扰,保证并发场景下数据传递的正确性。在web请求上下文中较为常见。
3. ThreadLocal 原理
3.1 代码结构
ThreadLocal 的原理要从它的set(T value)
、get()
方法的源码入手。在 set 值的时候,首先会获取当前线程一个的成员变量ThreadLocalMap
,ThreadLocalMap
的 key 是当前ThreadLocal
对象,value 是要存入的值。这个 key 和 value 会存到哪里呢?ThreadLocalMap
还有个内部类Entry
,这个Entry
继承了WeakReference
,key 赋值给弱引用,也就是当前的ThreadLocal
对象,value 则赋值给Entry的成员变量value
。ThreadLocalMap
也是一个哈希表(所谓哈希表,也叫散列表,它基于数组,通过某种哈希算法计算出一系列关键字对应的散列值,然后以这些散列值作为数组索引将数据存放到对应位置,达到快速查找的目的),它内部维护一个Entry
数组,来存储键值对。存数据的时候也是通过哈希函数计算ThreadLocal 对象对应的数组下标,然后放入Entry
数组中。
3.2 内存泄漏问题
ThreadLocal 会发生内存泄漏吗?我们结合代码慢慢分析。
在 2.1.1 节中有这样的代码:
public class ThreadLocalTest {private static ThreadLocal<String> threadLocal = new ThreadLocal<>();private void setUserId(String userId) {threadLocal.set(userId);}// ...
}
首先,我们new
了一个 ThreadLocal 对象,这里存在一个强引用:threadLocal
引用变量指向 ThreadLocal 对象。其次,当其他线程执行setUserId
方法时,ThreadLocal 的set
方法最终是把数据存到了ThreadLocalMap
中的Entry
,看源码我们会发现,存数据最终是调用Entry
的构造器Entry(ThreadLocal<?> k, Object v)
完成的,而k
这个参数是传入的this
对象,说明什么?我们使用 ThreadLocal 对象调用set
,那this
肯定是当前new
出来的 ThreadLocal 对象!再次说明,我们new
出来的 ThreadLocal 对象有两个引用指向它:
threadLocal
变量的强引用。- 在
Entry
中的弱引用。
此时再看一张图(这张图被广泛引用,感谢原图作者😂):
- 堆内存里面有个 ThreadLocal 对象,它被两个箭头指着,实线代表强引用,虚线代表弱引用。
- 有两个引用链,一个是我们手动创建的
threadLocal
的引用变量指向的,即图中的 ThreadLcoal Ref 对应示例代码中的threadLocal
变量;一个是由于调用了 ThreadLocal 的set
或get
方法,初始化了当前线程的ThreadLocalMap
,再初始化 Map 中的Entry
对象,再初始化Entry
对象中的 key 和 value,形成一个由当前线程对象到它内部变量的引用链,即上图中的 Current Thread Ref,它对应set
方法源码中的这一行Thread t = Thread.currentThread();
中的变量t
。
那问题来了,如果这个手动创建的 ThreadLocal 对象 的『引用变量』被回收了,那 ThreadLocal 对象 是不是只剩下Entry
中 key 的弱引用了?而弱引用的对象会随时被 GC 回收,即Entry
中的 key 会在 GC 后变为null
了。我们知道,ThreadLocalMap
的 key 是当前的 ThreadLocal 对象,那 key 为null
了之后,就无法获取到Entry
,也取不到 value 的值了。在Entry
对象没有被主动删除,或者当前线程没有终结的情况下,该Entry
一直处在一个由当前线程指向的强引用链中。由于这个Entry
获取不到,就一直占用着内存,又因为强引用不能被 GC 回收,所以这个Entry
就发生了内存泄漏。如果这个线程是一个普通线程,在线程终止的时候,整个线程对象被回收了,那内存泄漏的时间比较短;如果该线程一直不终止,比如线程池中的核心线程,那内存泄露问题就一直存在了。
注意,上面说的“如果这个手动创建的 ThreadLocal 对象 的『引用变量』被回收了”,应该会有人疑惑这种情况什么时候会发生呢?第一种情况,手动把这个引用变量置为null
,虽然概率小,但也不是没可能;第二种情况,引用变量是存在栈内存中,当方法执行完,就会立即回收栈内存中的引用变量,即堆内存中的实际对象失去引用指针了。这种情况就比如 ThreadLocal 是在方法中创建的局部变量。
3.3 为什么使用弱引用
Entry
的 key 使用弱引用有内存泄漏风险,那为什么 JDK 还是使用弱引用而不是强引用?
我们分两种情况讨论:
- key 使用强引用:ThreadLocal 的引用变量被回收了,这句话意味着什么呢?引用变量被回收了,意味着代码中不再使用 ThreadLocal 这个对象了,因为要使用 ThreadLocal 这个对象,我们需要用它的引用变量取调
set
、get
方法,现在引用变量没了,我们就用不了 ThreadLocal 这个对象了。但问题是,ThreadLocalMap
还持有ThreadLocal
对象的强引用,当前线程到Entry
的强引用链依然存在。注意,前面提到了,ThreadLocal 对象已经不再使用了,也就是说Entry
就获取不到了。如果Entry
没有手动删除,或者线程没有结束,这个没用的Entry
也会一直保留,依然发生内存泄漏(要明白内存泄漏是对象没用了,还存在内存中不被回收的情况)。 - key 使用弱引用:前面已经分析过了,ThreadLocal 的引用变量被回收了,
ThreadLocal
对象也被回收,导致Entry
的 key 变成null
,在没有手动删除Entry
或线程不结束时依然发生内存泄漏。
归根结底,由于ThreadLocalMap
的生命周期跟Thread
一样长,在 ThreadLocal 的引用变量消失后,如果线程不结束,原来的Entry
就不会回收,这就是内存泄漏的本质。虽然 ThreadLocal 在每次读写数据的时候,都会将key
为null
的Entry
清空,但是,既然 ThreadLocal 的引用变量都消失了,我们也没机会再set
或get
了。
那为什么使用弱引用?我也不知道!我还没想明白,如果正在阅读的你知道,请你告诉我下,谢谢😅。虽然ThreadLocalMap
的注释中解释了:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了帮助处理非常大和长期的使用,哈希表条目使用WeakReferences作为键。
我觉得没必要取纠结这个问题,只要规范的使用 ThreadLocal,几乎不会发生内存泄漏。
3.4 如何防止内存泄漏
- 把 ThreadLocal 对象申明为类变量。类变量的生命周期跟 JVM 是同步的,这样 ThreadLocal 的强引用就一直存在,不会被 GC 回收,
Entry
的key
就不会发生null
的情况了。 - 使用完 ThreadLocal 后,用
remove()
方法,清空当前ThreadLocal 对应的数据,对应的Entry
就不占内存了。
第一种情况虽热能避免Entry
的key为null
的情况,但是如果后续线程不再访问这个 key,且线程不结束时,这个 key 对应的数据也会一直存在内存中,容易造成内存溢出的问题。所以最好的办法就是在 ThreadLocal 使用完之后,使用remove()
方法清除数据。
4. ThreadLocal 如何存多个变量
上面的示例代码中,ThreadLocal 只存了一个变量,实际情况不可能只存一个吧,多个变量如何存,如何取?
要知道 ThreadLocal 使用set
方法存数据时,key 用的this
对象,就是当前正在使用的 ThreadLocal 对象,说明一个 ThreadLocal 对象,在一个线程中,只能存一个线程本地变量。多个线程虽然都是用的是一个 key,但是不同的线程用的是不同的ThreadLocalMap
。
第一种方案是多 new 几个 ThreadLocal 对象,每个 ThreadLocal 对象对应一个业务变量。
第二种方法就是在给 ThreadLocal 初始化一个HashMap
,这是最常规的做法。比如下面:
public class ThreadLocalTest {private static final ThreadLocal<Map<String, Object>> context =ThreadLocal.withInitial(HashMap::new);private String getUserId() {return String.valueOf(context.get().get("userId"));}private void setUserId(String userId) {context.get().put("userId", userId);}public void setUserName(String userName) {context.get().put("userName", userName);}public String getUserName() {return String.valueOf(context.get().get("userName"));}public static void main(String[] args) {ThreadLocalTest test = new ThreadLocalTest();for (int i = 1; i < 5; i++) {Thread thread = new Thread(() -> {String threadName = Thread.currentThread().getName();test.setUserId(threadName + "的userId");test.setUserName(threadName + "的userName");System.out.println("===执行业务代码===");System.out.println(threadName + "-->" + test.getUserId() + "," + test.getUserName());});thread.setName("线程" + i);thread.start();}}
}
一种可能的结果:
===执行业务代码===
线程2-->线程2的userId,线程2的userName
===执行业务代码===
线程4-->线程4的userId,线程4的userName
===执行业务代码===
线程3-->线程3的userId,线程3的userName
===执行业务代码===
线程1-->线程1的userId,线程1的userName
5. 为什么用 ThreadLocal
5.1 ThreadLocal的使用场景
线程的上下文传递。企业中最常见的是应用到web请求的上下文,一个 Http 请求会经过一系列拦截器,过滤器最后到达服务层,在这个调用链路中,会频繁的使用到一些公共数据,如用户信息或请求的ID,把这些公共数据放到 ThreadLocal 中,会在请求的链路中非常方便的使用这些信息。
还有一些框架中会使用 ThreadLocal 来管理数据库连接,避免了线程之间的竞争。比如 Mybatis 就是用 ThreadLocal 来存储Sqlsession对象。
5.2 使用 ThreadLocal 的好处
使用 ThreadLocal 的好处是并发场景下减少了同一个线程内多个函数或组件之间传递公共变量的复杂度,且提高了使用这些共享变量的安全性。
相关文章:

ThreadLocal的应用
1. ThreadLocal 是什么 JDK 对ThreadLocal的描述为: 此类提供线程局部变量。这些变量与普通变量的不同之处在于,每个访问一个变量的线程(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有…...

中值滤波_中值滤波原理
均值滤波是典型的线性滤波算法,它是指在图像上对目标像素给一个模板,该模板包括了其周围的临近像素(以目标象素为中心的周围8个象素,构成一个滤波模板,即去掉目标象素本身).再用模板中的全体像素的平均值来代替原来像素值.均值滤波也称为线性滤波,其采用的主要方法为领域平均法…...

day15 - 使用图像金字塔进行图像拼接
在我们之前的学习过程中,使用的都是恒定大小的图像,但是在某些情况下,我们需要使用不同分辨率的(相同)图像。例如,当在图像中搜索某些东西(例如人脸)时,我们不确定对象将…...

算法修炼之筑基篇——筑基一层初期(解决01背包问题)
✨博主:命运之光 ✨专栏:算法修炼之练气篇 ✨博主的其他文章:点击进入博主的主页 前言:学习了算法修炼之练气篇想必各位蒟蒻们的基础已经非常的扎实了,下来我们进阶到算法修炼之筑基篇的学习。筑基期和练气期…...

JVM的空间结构
目录 一、概述 二、分类 1.程序计数器区域(Program Counter Register): 2.Java虚拟机栈(Stack): 3.堆区(Heap): 4.方法区(Method Area): 5.本地方法栈(Native Method Stack): 一、概述 JVM分为5个主要区域&…...

图像分割的常用算法
图像分割是指将一幅图像划分成多个子区域或像素集合的过程,其中每个子区域或像素集合具有一定的统计特征或语义信息。图像分割是图像处理中的基础任务,其应用涵盖了医学影像、计算机视觉、机器人技术等多个领域。常用的图像分割算法包括: 1.…...

AI歌手真的可以吗
你听过AI歌手吗?近日,“AI孙燕姿”火遍全网,AI孙燕姿翻唱林俊杰的《她说》、周董的《爱在西元前》、赵雷的《成都》等等歌曲让网友听了直呼:“听了一晚上,出不去了。”你认为AI歌手会取代流行歌手成为主流吗࿱…...

Kubernetes高级存储
Kubernetes高级存储 PV PVC k8s支持的存储系统很多,全部掌握不现实。为了屏蔽底层存储实现的细节,方便用户使用,k8s引入PV和PVC两种资源对象。 PV(Persistent Volume)持久化卷,对底层共享存储的抽象,一般由k8s管理员进…...

云原生之使用Docker部署docker-compose-ui工具
云原生之使用Docker部署docker-compose-ui工具 一、Docker Compose UI介绍二、检查本地docker环境1.检查系统版本2.检查docker状态 三、下载Docker Compose UI镜像四、部署Docker Compose UI服务1.新建安装目录2.创建Docker Compose UI容器3.检查Docker Compose UI容器状态4.查…...

文心一言 vs GPT4
本周真是科技爱好者的狂欢节。GPT4和文心一言接连发布,AI工具已经开始走进千家万户。 拿文心一言发布会上的几个问题调戏了 GPT4 一下,看看表现如何。 第一个为文心的回答,第二个为GPT4 的回答。 1. 可以总结一下三体的核心内容吗…...

Tcl-5. format 命令
format 命令和 C 语言中的 printf 和 sprintf 命令类似。它根据一组格式说明来格式化字符 串。此命令不会改变被操作字符串的内容。 [语法]:format spec value1 value2 ... spec 变元包含了格式说明关键词和附加文字。使用%来引入一个关键词,后跟 0 个…...

BloombergGPT: 首个金融垂直领域大语言模型
BloombergGPT: 首个金融垂直领域大语言模型 Bloomberg 刚刚发布了一篇研究论文,详细介绍了他们最新的突破性技术 BloombergGPT。BloombergGPT是一个大型生成式人工智能模型,专门使用大量金融数据进行了训练,以支持金融行业自然语言处理 (NLP…...

CMake深度解析:掌握add_custom_command,精通Makefile生成规则
CMake深度解析:掌握add_custom_command,精通Makefile生成规则 1. CMake简介与基础知识1.1 CMake的基本概念(CMake Basic Concepts)1.1.1 项目(Project)1.1.2 目标(Target)1.1.3 命令…...

基于Yolov5目标检测的物体分类识别及定位(二) -- yolov5运行环境搭建及label格式转换
刚开始跟着网上的教程做,把环境安装错了,后来直接用GitHub的官方教程来安装环境。 地址是yolov5官方团队代码及教程,看readme文件就可以。 系列文章: 基于Yolov5目标检测的物体分类识别及定位(一) -- 数据集…...

Office project 2019安装
哈喽,大家好。今天一起学习的是project 2019的安装,Microsoft Office project项目管理工具软件,凝集了许多成熟的项目管理现代理论和方法,可以帮助项目管理者实现时间、资源、成本计划、控制。有兴趣的小伙伴也可以来一起试试手。…...

【leetcode-mysql】1251. 平均售价
题目: Table: Prices ---------------------- | Column Name | Type | ---------------------- | product_id | int | | start_date | date | | end_date | date | | price | int | ---------------------- (product_id,start_date,end_dat…...

Razor代码复用
1.布局(Layout)复用 Layout的使用,就像WebForm的模板页一样,甚至会更加简单,更加方便和明了。 要使用Layout,首先要在模板页相应的位置添加RenderBody()方法: <!DOCTYPE html><html la…...

PRL:上海交大张文涛团队实现量子材料相关突破
来源:上海交通大学 近期,上海交通大学物理与天文学院张文涛研究组利用自行研制的高能量和高时间分辨率角分辨光电子能谱系统对量子材料1T-TiSe₂电子结构进行了超快激光操控研究。利用超快光激发与电荷密度波相有关的相干声子,引起晶格内原子…...

impala中group_concat()函数无法对内容进行order by
描述: 使用的是impala数据库,假设有四笔数据,是无序的,业务上要求将其行转列成一行数据,并且里面的数据要按从小到大排序。 过程: 猜测: 数据库Oracle、Mysql、MSsql等支持group_concat中使…...

MySQL 数据库全局变量中文解释
NameValueauto_increment_incrementAUTO_INCREMENT 字段值的自增长步长值。auto_increment_offsetAUTO_INCREMENT 字段值的初始值。autocommit指示新连接的默认提交模式是否启用。automatic_sp_privileges控制是否在存储过程上创建或更改时自动分配特定权限。back_log在开始拒绝…...

设计模式之~状态模式
状态模式(State),当一个对象的内部状态改变时允许改变其行为,这个对象看起来像是改变了其类。 能够让程序根据不同的外部情况来做出不同的响应,最直接的方法就是在程序中将这些 可能发生的外部情况全部考虑到ÿ…...

【21JavaScript break 和 continue 语句】JavaScript中的break和continue语句:控制循环流程的关键技巧
JavaScript break 和 continue 语句 在JavaScript中,break和continue是两个关键字,用于控制循环结构的执行流程。 break语句 break语句用于中断循环并跳出循环体,使程序执行流程继续到循环之后的下一行代码。 在for循环中使用break for (…...

【SpringBoot】 设置随机数据 用于测试用例
个人简介:Java领域新星创作者;阿里云技术博主、星级博主、专家博主;正在Java学习的路上摸爬滚打,记录学习的过程~ 个人主页:.29.的博客 学习社区:进去逛一逛~ 设置随机数据——常用于测试用例 SpringBoot设…...

chatgpt赋能python:Python如何获取微信聊天记录
Python如何获取微信聊天记录 作为世界上最受欢迎的即时通讯工具之一,微信被大量用户使用。然而,微信聊天记录的备份和管理是一个重要的问题,特别是对于那些需要在工作和个人生活中快速查找重要信息的人来说。 幸运的是,Python编…...

VP记录:Codeforces Round 599 (Div. 2) A~D
传送门:CF 前提提要:无 A题:A. Maximum Square 刚开始的第一个想法是排序然后二分答案.但是一看范围才1000,果断直接使用暴力枚举. 考虑枚举最终的答案,然后记录有多少个 a i ai ai大于此值,然后判断能否构成一个正方形即可. #include <bits/stdc.h> using namespace…...

01-项目介绍
1、特色与亮点 千万级流量的大型分布式系统架构设计。 高性能、高并发、高可用场景解决方案。 2、项目安排 架构搭建,使用前后端分离架构。 功能开发,实现基本的选座排队购票功能。 引入高并发技术,实现高性能抢票。 3、项目收获 学习…...

《Python编程从入门到实践》学习笔记06字典
alien_0{color:green,points:5} print(alien_0[color]) print(alien_0[points])green 5 alien_0{color:green,points:5} new_pointsalien_0[points] print(fyou just earned {new_points} points!)you just earned 5 points! #添加键值对 alien_0{color:green,points:5} prin…...

为什么说程序员和产品经理一定要学一学PMP
要回答为什么说程序员和产品经理一定要学一学PMP?我们得先看一下PMP包含的学习内容。PMP新版考纲备考参考资料绝大多数涉及IT项目的敏捷管理理念。主要来源于PMI推荐的10本参考书: 《敏捷实践指南(Agile Practice Guide)》 《项目…...

LearnOpenGL-高级OpenGL-9.几何着色器
本人初学者,文中定有代码、术语等错误,欢迎指正 文章目录 几何着色器使用几何着色器造几个房子爆破物体法向量可视化 几何着色器 简介 在顶点和片段着色器之间有一个可选的几何着色器几何着色器的输入是一个图元(如点或三角形)的一…...

8.视图和用户管理
目录 视图 基本使用 用户管理 用户 用户信息 创建用户 删除用户...