深入理解ThreadLocal原理

目录

    • 1- 什么是ThreadLocal ?
    • 2- ThreadLocal的作用?
      • ThreadLocal实现线程间资源隔离
      • ThreadLocal实现线程内资源共享
    • 3- ThreadLocal 原理
      • 3-1 ThreadLocalMap
      • 3-2 ThreadLocalMap的扩容
        • 🔑1. 为什么会发生扩容?
        • 🔑2. ThreadLocalMap索引计算?
        • 🔑3. ThreadLocalMap扩容?
      • 3-3 ThreadLocalMap如何解决哈希冲突?
    • 4- ThreadLocalMap 键的弱引用
        • 🔑为什么ThreadLocal的key设计成弱引用?
    • 5- ThreadLocal 中 value 内存回收
        • ❌1.值回收场景1:get(不推荐)
        • ❌2.值回收场景2:set(不推荐)
        • 🔑3.值回收场景3:remove 防止内存泄漏
    • 补充知识
      • Java中的弱引用和强引用
      • ThreadLocal的弱引用


1- 什么是ThreadLocal ?

  • ThreadLocal是Java中的一个工具类,它提供了线程局部变量,即这些变量对于使用它的每个线程来说都是独立的。每个线程都可以通过ThreadLocal存储、访问和更新自己的变量副本,而不会与其他线程的变量副本冲突。

2- ThreadLocal的作用?

ThreadLocal 两大作用

  • ① 线程间资源隔离
  • ② 线程内资源共享

ThreadLocal实现线程间资源隔离

public class ThreadLocalExample {// 创建一个 ThreadLocal 实例,用于存储每个线程的计数器private static final ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 0);public static void main(String[] args) {// 创建三个线程,每个线程都会更新自己的计数器for (int i = 0; i < 3; i++) {Thread thread = new Thread(() -> {// 获取当前线程的计数器int counter = threadLocalCounter.get();// 更新计数器的值counter += 5;// 将更新后的值重新设置到 ThreadLocal 中threadLocalCounter.set(counter);// 打印当前线程的计数器值System.out.println(Thread.currentThread().getName() + " counter: " + threadLocalCounter.get());}, "Thread-" + i);thread.start();}}
}

输出:
image.png
分析

  • threadLocalCounter是一个ThreadLocal实例,它通过withInitial方法初始化,为每个线程提供了一个初始值0。
  • 在输出结果中,我们可以看到每个线程都打印出了counter: 5 ,这表明每个线程都成功地将自己的计数器从0增加到了5,而且这个增加操作没有影响到其他线程的计数器。这正是ThreadLocal实现线程间资源隔离的效果。

ThreadLocal实现线程内资源共享

public class ThreadLocalExample2 {// 创建一个 ThreadLocal 实例,用于存储每个线程的会话信息private static final ThreadLocal<String> threadLocalSession = new ThreadLocal<>();public static void main(String[] args) {// 创建两个线程,模拟两个用户的会话for (int i = 1; i <= 2; i++) {final int userId = i;Thread thread = new Thread(() -> {// 在 ThreadLocal 中设置当前线程的会话信息String session = "UserSessionForUser" + userId;threadLocalSession.set(session);// 执行线程内的不同方法,它们都可以访问到同一个会话信息performAction("add item to cart");performAction("checkout");performAction("logout");// 清理资源,避免内存泄漏threadLocalSession.remove();}, "Thread-for-User-" + userId);thread.start();}}// 模拟一个操作,该操作需要访问会话信息private static void performAction(String action) {// 从 ThreadLocal 获取当前线程的会话信息String session = threadLocalSession.get();System.out.println(Thread.currentThread().getName() + " performing action: " + action + " with " + session);}}

输出:
image.png
分析

  • 在输出结果中,我们可以看到每个线程(例如Thread-for-User-1和Thread-for-User-2)都能够连续执行add item to cart、checkout 和 logout操作,并且每个操作都使用了与该线程关联的会话信息(UserSessionForUser1和UserSessionForUser2)。这表明ThreadLocal确实实现了同一线程内的资源共享。

3- ThreadLocal 原理

3-1 ThreadLocalMap

  • **实际上 ThreadLocal 实现了资源的关联,本质上是通过 **ThreadLocalMap实现的线程间资源隔离

其原理是,每个线程内有一个ThreadLocalMap类型的成员变量,用来存储资源对象

  • ① 调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的 ThreadLocalMap集合中
  • ② 调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值
  • ③ 调用remove方法,就是以ThreadLocal自己作为 key,移除当前线程关联的资源值

3-2 ThreadLocalMap的扩容

🔑1. 为什么会发生扩容?
  • 一个线程可以拥有多个 ThreadLocal 对象,每个 ThreadLocal 对象都可以存储在同一个线程的 ThreadLocalMap 中,而且彼此独立。
  • 这就是为什么 ThreadLocalMap 的容量是16(默认初始容量)并且具有扩容机制的原因。扩容机制确保了当一个线程使用多个 ThreadLocal 对象时,ThreadLocalMap 能够适应更多的条目。

例子

public class ThreadLocalExample {// 创建两个不同的 ThreadLocal 实例private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<>();private static final ThreadLocal<String> threadLocal2 = new ThreadLocal<>();public static void main(String[] args) {// 在同一个线程中为不同的 ThreadLocal 实例设置值threadLocal1.set("Value for ThreadLocal 1");threadLocal2.set("Value for ThreadLocal 2");// 每个 ThreadLocal 实例的值是独立的System.out.println("ThreadLocal 1 contains: " + threadLocal1.get());System.out.println("ThreadLocal 2 contains: " + threadLocal2.get());}
}
  • 在这个例子中,我们有两个 ThreadLocal 实例。即使它们都在同一个线程中使用,每个 ThreadLocal 实例也会在 ThreadLocalMap 中占据不同的槽位。因此,threadLocal1.set()threadLocal2.set() 操作不会相互覆盖,因为它们是两个独立的条目。
  • 如果一个线程使用的 ThreadLocal 实例数量超过了 ThreadLocalMap 的当前容量,ThreadLocalMap 就会根据需要进行扩容,以便为新的 ThreadLocal实例提供空间。
🔑2. ThreadLocalMap索引计算?
  • 索引计算:当线程每创建一个新的 ThreadLocal 对象时候,会为当前 ThreadLocal对象分配一个 哈希值,最初哈希值为 0 因此,如下图 ThreadLocal1 插入的位置为 0
  • 此时如果又创建 ThreadLocal2 会在 0 的基础上加一个 数字,会根据这个数字计算出当前ThreadLocal2 的索引,比如计算出索引为 7 所以,ThreadLocal2 存储在索引为 7 的位置
  • 同理对于 ThreadLocal3,计算出下标位 11

image.png

🔑3. ThreadLocalMap扩容?
  • ThreadLocalMap 的 capacity 为 16
  • ThreadLocalMap 的 扩容因子 为 2/3

因此大概在什么时候发生扩容?

  • 16 * 2/3 =10.6
  • 大约在第 10 个元素时,发生扩容,容量翻倍

若当前容量为16,则插入第 10 个元素时候,会发生扩容,扩容后容量为之前两倍,且会重新计算索引
image.png

  • ThreadLocalMap 扩容时,它的容量(即内部数组的大小)增加,通常是翻倍。由于哈希表的索引是通过哈希码与当前容量相关的运算得到的,增加容量后,原有元素的索引可能会发生变化。这是因为索引计算通常涉及到哈希码与容量的某种运算(如模运算),容量的改变意味着这个运算的结果也可能改变。
  • 为了保持哈希表的正确性和效率,扩容后需要重新计算所有元素的索引,并将它们放置到正确的位置。这个过程称为“重新哈希”(rehashing)。重新哈希确保了元素在新的内部数组中仍然按照其哈希码分布,同时也解决了由于容量增加而可能导致的哈希冲突减少的情况。

3-3 ThreadLocalMap如何解决哈希冲突?

  • HashMap、ConcurrentHashMap、HashTable 都是使用拉链法解决哈希冲突
  • ThreadLocalMap使用的开放寻址法来解决哈希冲突

例如对于上述插入结果,如果再新加入一个 ThreadLocal11,固定哈希码从 0 开始,此时发现 0 位置被 ThreadLocal1占用,此时会寻找下一个地址,即寻找到 1 的位置,因此 ThreadLocal11 会插入到 索引为 1 的位置。
image.png


4- ThreadLocalMap 键的弱引用

image.png

  • 可以看到如下 ThreadLocalMap的源码,键和值为一个 Entry 类型
  • Entry 继承了 弱引用,并在构造中调用 super,因此 key 是一个弱引用类型
static class Entry extends WeakReference<ThreadLocal<?>>
  • Entry的构造函数
Entry(ThreadLocal<?> k, Object v) {super(k);value = v;
}
🔑为什么ThreadLocal的key设计成弱引用?
  • ① Thread 可能需要长时间运行(如线程池中的线程),如果 key 不再使用,需要在内存不足(GC)时释放其占用的内存
  • ② 但 GC 仅是让 key 的内存释放,后续还要根据 key 是否为 null 来进一步释放内存
    • 获取 key 发现 null key
    • set key 时,会使用启发式扫描,清除临近的 null key,启发次数与元素个数,是否发现 null key 有关
    • remove时(推荐),因为一般使用 ThreadLocal 时都把它作为静态变量,因此 GC 无法回收

5- ThreadLocal 中 value 内存回收

  • 假设有如下场景,ThreadLocal中的 key 被 GC (用黑色表示key内存被释放了),而此时 三个 value值还存在,则值释放的场景有三种

image.png

❌1.值回收场景1:get(不推荐)
  • 获取 key 发现 null key,此时对应的内存也会被释放,比如 get操作执行到 索引 7 的位置此时发现 7 位置的 key 为null,此时会释放掉 value2的内存,同时会放进去一个新 key

image.png

❌2.值回收场景2:set(不推荐)
  • set key 时,会使用启发式扫描,清除临近的 null key,启发次数与元素个数,是否发现 null key 有关
  • 启发式是指
    • ① 如果元素个数多,则扫描范围大一点
    • ② 是否发现 null key,如果发现 null key 则扫描范围大一点

get 方法和 set方法有局限性

  • get方法:当调用get方法时,如果对应的ThreadLocal对象已经被回收,ThreadLocalMap中的条目的键会是null。虽然get方法会检查并清理掉这些键为null的条目,但如果不频繁调用get方法,这些条目就会一直存在,占用内存。
  • set方法:set方法在添加新的线程局部变量时,会尝试清理已经被回收的ThreadLocal对象的条目(键为null的条目)。然而,如果一个线程不再设置新的线程局部变量,那么这种清理机制就不会被触发。

总结来说,虽然get和set方法在一定程度上可以减轻内存泄漏的问题(通过清理键为null的条目),但它们不能完全解决问题,因为它们依赖于对ThreadLocal变量的频繁访问。只有通过显式调用remove方法,才能确保ThreadLocalMap中相关的条目被及时清理,从而避免内存泄漏。

🔑3.值回收场景3:remove 防止内存泄漏
  • ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
  • 这样一来,ThreadLocalMap 中就会出现 keynull 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。
  • ThreadLocalMap实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法

补充知识

Java中的弱引用和强引用

  1. 强引用(Strong Reference)
  • 强引用是Java中最常见的引用类型。如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会回收这种对象。
  • 只要obj引用存在,所指向的对象就不会被回收。
Object obj = new Object(); // obj是一个强引用
  1. 软引用(Soft Reference)
  • 软引用是用来描述一些还有用但非必需的对象。在系统将要发生内存溢出异常之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 软引用可以用来实现内存敏感的高速缓存。
Object obj = new Object();
SoftReference<Object> softRef = new SoftReference<Object>(obj);
obj = null; // 取消强引用
  1. 弱引用(Weak Reference)
  • 弱引用也是用来描述非必需对象的,但是其强度比软引用更弱一些,被弱引用关联对象只能生存到下一次垃圾回收发生之前。当垃圾回收器工作时,无论当前内存空间足够与否,都会回收掉只被弱引用关联的对象。
  • 弱引用通常用于实现规范映射(Canonicalizing mappings)等,例如WeakHashMap。
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<Object>(obj);
obj = null; // 取消强引用

ThreadLocal的弱引用

  • 当一个类的构造器中调用了super(k),并且这个类是WeakReference的子类,这意味着这个类在构造时将传入的对象 k 作为一个弱引用来处理。
  • 因此在 ThreadLocalMap 源码中的 key 是一个弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}

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

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

相关文章

将图像转换为ASCII艺术形式

将图像转换为ASCII艺术形式 在本文中&#xff0c;我们将介绍一个使用OpenCV库将图像转换为ASCII艺术形式的简单程序。ASCII艺术是一种使用字符来表现图像的艺术形式&#xff0c;通过在终端或文本文件中显示字符的不同密度和颜色来模拟图像。这种技术已经存在了几十年&#xff…

【MySQL】7.MHA高可用配置及故障切换

什么是MHA MHA&#xff08;MasterHigh Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件 mha用于解决mysql的单点故障问题&#xff1b; 出现故障时&#xff0c;mha能在0~30秒内自动完成故障切换&#xff1b; 并且能在故障切换过程中&#xff0…

史上最强 PyTorch 2.2 GPU 版最新安装教程

一 深度学习主机 1.1 配置 先附上电脑配置图&#xff0c;如下&#xff1a; 利用公司的办公电脑对配置进行升级改造完成。除了显卡和电源&#xff0c;其他硬件都是公司电脑原装。 1.2 显卡 有钱直接上 RTX4090&#xff0c;也不能复用公司的电脑&#xff0c;其他配置跟不上。…

ARM FVP平台的terminal窗口大小如何设置

当启动ARM FVP平台时&#xff0c;terminal窗口太小怎么办&#xff1f;看起来非常累眼睛&#xff0c;本博客来解决这个问题。 首先看下ARM FVP平台对Host主机的需求&#xff1a; 通过上图可知&#xff0c;UART默认使用的是xterm。因此&#xff0c;我们需要修改xterm的默认字体设…

C++语言学习(一)——关键字、命名空间、输入输出

1. C关键字 C总计63个关键字&#xff0c;C语言32个关键字 2. 命名空间 在C/C中&#xff0c;变量、函数和后面要学到的类都是大量存在的&#xff0c;这些变量、函数和类的名称将都存在于全局作用域中&#xff0c;可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本…

yolov5关键点检测-实现溺水检测与警报提示(代码+原理)

基于YOLOv5的关键点检测应用于溺水检测与警报提示是一种结合深度学习与计算机视觉技术的安全监控解决方案。该项目通常会利用YOLOv5强大的实时目标检测能力&#xff0c;并通过扩展或修改网络结构以支持人体关键点检测&#xff0c;来识别游泳池或其他水域中人们的行为姿态。 项…

Java入门学习Day04

本篇文章主要介绍了&#xff1a;如何输入数据、字符串拼接、自增自减运算符、类型转换&#xff08;int&#xff0c;double等&#xff09; CSDN&#xff1a;码银 公众号&#xff1a;码银学编程 一、键盘输入练习 Scanner是Java中的一个类&#xff0c;用于从控制台或文件中读…

DOTS:Burst

目录 一&#xff1a;简介 1.1 Getting started 1.2 C# language support 1.2.1 HPC# overview 1.2.1.1 Exception expressions 1.2.1.2 Foreach and While 1.2.1.3 Unsupported C# features in HPC# 1.2.2 Static read-only fields and static constructor support 1.…

STM32-03基于HAL库(CubeMX+MDK+Proteus)输入检测案例(按键控制LED)

文章目录 一、功能需求分析二、Proteus绘制电路原理图三、STMCubeMX 配置引脚及模式&#xff0c;生成代码四、MDK打开生成项目&#xff0c;编写HAL库的按键检测代码五、运行仿真程序&#xff0c;调试代码 一、功能需求分析 搭建完成开发STM32开发环境之后&#xff0c;开始GPIO…

LC 110.平衡二叉树

110. 平衡二叉树 给定一个二叉树&#xff0c;判断它是否是高度平衡的二叉树。 本题中&#xff0c;一棵高度平衡二叉树定义为&#xff1a; 一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。 示例 1&#xff1a; 输入&#xff1a; root [3,9,20,null,null,15,7]…

补充知识

补充知识1 内存的本质是对数据的临时存储 内存与磁盘进行交互时&#xff0c; 最小单位是4kb叫做页框(内存)和页帧(磁盘) 也就是&#xff0c; 如果我们要将磁盘的内容加载到内存中&#xff0c; 可是文件大小只有1kb&#xff0c; 我们也要拿出4kb来存他&#xff0c; 多余的就直…

基于Leaflet.js和Turf.js的等值线区间自定义及颜色自适应实践

目录 前言 一、Turf.js等值线相关制作 1、生成方法 2、主要参数 二、实际案例开发 1、新建展示页面 2、等值线生成 3、基于Leaflet的再优化 总结 前言 在气象方面的GIS应用当中&#xff0c;会根据实际的工作需要建立不同的监测站点。气象监测站的主要功能包括&#xff1…

pnpm--安装与使用

原文网址&#xff1a;pnpm--安装与使用-CSDN博客 简介 本文介绍pnpm的安装与使用。 pnpm由npm/yarn衍生而来&#xff0c;解决了npm/yarn内部潜在的bug&#xff0c;极大的优化了性能&#xff0c;扩展了使用场景&#xff0c;被誉为“最先进的包管理工具”&#xff0c;速度快、…

变量重名情况

变量重名 变量的使用规则&#xff1a;就近原则 第一种情况&#xff1a;局部变量和成员变量重名&#xff0c;使用this关键字访问成员变量 第二种情况&#xff1a;子类成员变量和父类成员变量重名&#xff0c;使用super关键字访问父类成员变量 // 父类 public class Fu {int …

舞蹈网站制作分享,舞蹈培训商城网站设计案例分享,wordpress主题分享

嘿&#xff0c;朋友们&#xff01;今天我要跟你们唠一唠一个超级酷炫的舞蹈培训商城网站设计案例。 咱先说说这个网站的目标哈&#xff0c;那就是得让喜欢舞蹈的小伙伴们能够轻轻松松找到自己心水的课程和商品。 那制作过程都有啥呢&#xff1f;别急&#xff0c;听我慢慢道来。…

C#常见Winform窗体效果

目录 1&#xff0c;窗体闪烁。 2&#xff0c;透明非矩形的窗体。 3&#xff0c;窗口显示&#xff0c;退出呈现平滑效果。 4&#xff0c;窗体不在任务栏中显示&#xff1a; 1&#xff0c;窗体闪烁。 /// <summary>/// 窗体闪烁/// </summary>/// <param na…

在c# 7.3中不可用,请使用9.0或更高的语言版本

参考连接&#xff1a;在c# 7.3中不可用,请使用8.0或更高的语言版本_功能“可为 null 的引用类型”在 c# 7.3 中不可用。请使用 8.0 或更高的语言版本-CSDN博客https://blog.csdn.net/liangyely/article/details/106163660 [踩坑记录] 某功能在C#7.3中不可用,请使用 8.0 或更高的…

STM32 | 通用同步/异步串行接收/发送器USART带蓝牙(第六天原理解析)

STM32 第六天 一、 USART 1、USART概念 USART:(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步串行接收/发送器 USART是一个全双工通用同步/异步串行收发模块,该接口是一个高度灵活的串行通信设备 处理器与外部设备通信的两种方式: u并行通信(…

摸鱼123

摸鱼https://toyaml.com/windowsupdate.html

【漏洞复现】用友NC-Cloud文件服务器用户登陆绕过漏洞

Nx01 阅读须知 如棠安全的技术文章仅供参考&#xff0c;此文所提供的信息只为网络安全人员对自己所负责的网站、服务器等&#xff08;包括但不限于&#xff09;进行检测或维护参考&#xff0c;未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此文所提供的…