让我们谈谈你对 ThreadLocal 的理解

news2025/2/26 13:41:27

介绍 ThreadLocal

从 JDK1.2 开始,ThreadLocal 是一个被用来存储线程本地变量的类。在 ThreadLocal 中的变量在线程之间是独立的。当多个线程访问 ThreadLocal 中的变量,它们事实上访问的是自己当前线程在内存中的变量,这能确保这些变量是线程安全的。

我们通常使用 ThreadLocal 解决线程中的变量冲突问题。事实上,解决这类问题,我们通常考虑使用 synchronized。

例如,当解决 SimpleDateFormat 的线程安全问题时,SimpleDateFormat 不是线程安全的。不管 format() 方法或 parse() 方法,它使用了自己的内部 Calendar 对象。format() 方法设置时间,parse() 方法调用 Calendar.first.clear() 方法,再调用 Calendar 类的 set() 方法。如果一个线程刚刚调用 set(),期间有另一个线程直接调用了 clear() 方法,会导致 parse() 方法出现问题。

第一种解决办法,对使用了 SimpleDateformat 的方法加 synchronized,尽管线程安全有了保障,但是效率(吞吐量)变低了。一段时间内只有一个线程能使用这个方法。

 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 public static synchronized String formatDate(Date date){
     return simpleDateFormat.format(date);
 }

第二种解决办法,将 SimpleDateFormat 对象放入 ThreadLocal,所以,每一个线程都有了自己的 SimpleDateFormat 对象。它们不会互相干扰,保证了线程安全。

 private static final ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(
     () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
 );public static String formatDate(Date date)
 {
    return simpleDateFormatThreadLocal.get().format(date);
 }

ThreadLocal 的基本用法

下面看看如何使用 ThreadLocal:

 ThreadLocal<Integer> threadLocal99 = new ThreadLocal<Integer>();
 threadLocal99.set(3);
 int num = threadLocal99.get();
 System.out.println("Number:"+num);
 threadLocal99.remove();
 System.out.println("Digital Empty:"+threadLocal99.get());

运行结果:

 Number: 3
 Number Empty: null

ThreadLocal 非常容易使用。main 线程向 ThreadLocal 中放入了多个变量,它们在线程执行期间都能访问。当线程执行结束,变量都将被销毁。只要没有调用 remove() 方法,当前线程都能从 ThreadLocal 获取到数据。

因为 ThreadLocal 存放在当前执行线程中,ThreadLocal 中的变量值仅能在当前线程(当前线程的子线程也可以获取到)中使用,这保证了线程安全。

让我们看一下 ThreadLocal 类的 set() 方法的源码:

 public void set(T value) {
     // Get the current thread
     Thread t = Thread.currentThread();
     // Get ThreadLocalMap
     ThreadLocal.ThreadLocalMap map = getMap(t);
     // Whether the ThreadLocalMap object is empty, if it is not empty, put the data directly into the ThreadLocalMap
     if (map != null)
         map.set(this, value);
     else
         createMap(t, value); // If the ThreadLocalMap object is empty, create the object first, and then assign the value.
 }

我们看到变量都存储在 ThreadLocalMap 变量中。所以,ThreadLocalMap 从哪里来的?

 ThreadLocalMap getMap(Thread t) {
     return t.threadLocals;
 }
 public class Thread implements Runnable {
     ......
     /* ThreadLocal values pertaining to this thread.This map is maintained
      * by the ThreadLocal class.*/
     ThreadLocal.ThreadLocalMap threadLocals = null;
     ......
 }

通过上面的源码,我们可以发现 ThreadLocalMap 变量是当前执行线程的一个变量,所以,存储在ThreadLocal 中的数据其实都是存放在当前线程的 threadLocals 变量中。threadLocals 变量存放在当前线程对象中。对于另一个线程,就是另一个线程对象。另一个线程对象中的数据是不能获取到的,所以 ThreadLocal 是隔离的。

ThreadLocalMap 如何存储数据呢?

ThreadLocalMap 是 ThreadLocal 的内部类。尽管名字中包含 Map,它并没有实现 Map 接口,但是结构和 Map 类似。

image-20220707231132012.png

ThreadLocalMap 事实上是一个由 Entry 组成的 array,Entry 是 ThreadLocalMap 的一个内部类,继承了 WeakReference,同时将 Entry 的 key 设置成了 ThreadLocal 对象,并且还将 key 设置成了弱引用。ThreadLocalMap 的内部结构大概就是一个 Entry 的集合,其中 Entry 中的 key 和 value 的类型是固定的。

image-20220707231808611.png

它和真正的 Map 还是有一些区别的,没有链表,这就意味着解决 hash 冲突的方法和 HashMap 不同(HashMap 采用的是拉链法,ThreadLocalMap 采用的是线性探测)。

一个线程中可以创建多个 ThreadLocal 对象,多个 ThreadLocal 对象可以存储多个数据,这些数据将会存储在 ThreadLocalMap 的 array 中。

接下来看看 ThreadLocalMap 中比较特别的方法 set():

 /**
  * Set the value associated with key.
  * @param key the thread local object
  * @param value the value to be set
  */
 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;
     // Position in the array
     int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];
          e != null;
          e = tab[i = nextIndex(i, len)]) {
         ThreadLocal<?> k = e.get();
         // If the current position is not empty, and the key at the current position is equal to the passed key, then the data at the current position will be overwritten
         if (k == key) {
             e.value = value;
             return;
         }
         // If the current position is empty, initialize an Entry object and place it at the current position.
         if (k == null) {
             replaceStaleEntry(key, value, i);
             return;
         }
     }
     // If the current position is not empty, and the key at the current position is not equal to the key to be assigned, then the next empty position will be found and the data will be placed directly in the next empty position.
     tab[i] = new Entry(key, value);
     int sz = ++size;
     if (!cleanSomeSlots(i, sz) && sz >= threshold)
         rehash();
 }

我们可以从 set() 方法中看出,在代码逻辑层面分为了 4 步:

第一步,通过对 ThreadLocal 对象的 hash 值和 array 的数组长度求 AND,得到数据放入当前 array 的那个位置

第二步,判断当前位置是否为空,如果为空,直接初始化一个 Entry 对象并将其放入这个位置

第三步,如果当前位置不为空,并且当前位置的 Entry 的 key 和传入的 key 相等,将当前位置的数据覆盖掉

第四步,如果当前位置不为空,并且当前位置的 Entry 的 key 和传入的 key 不相等,于是向后查找下一个不为空的位置,将数据放入空位置中(当遍历到 array 的末尾,会执行扩容操作)

get() 方法采用的是相同的逻辑。首先通过传入的 ThreadLocal 的 hash 值获取到元素在 Entry Array 中的位置,然后比较当前位置的 Entry 的 key 是否和 传入的 ThreadLocal 对象相同,如果相同,直接返回当前位置的数据,如果不相同,就需要判断 key 是否和 array 中的下一个元素相同。

 private Entry getEntry(ThreadLocal<?> key) {
     int i = key.threadLocalHashCode & (table.length - 1);
     Entry e = table[i];
     if (e != null && e.get() == key)
         return e;
     else
         return getEntryAfterMiss(key, i, e);
 }
 /**
  * Version of getEntry method for use when key is not found in
  * its direct hash slot.
  *
  * @param  key the thread local object
  * @param  i the table index for key's hash code
  * @param  e the entry at table[i]
  * @return the entry associated with key, or null if no such
  */
 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
     Entry[] tab = table;
     int len = tab.length;
     while (e != null) {
         ThreadLocal<?> k = e.get();
         if (k == key)
             return e;
         if (k == null)
             expungeStaleEntry(i);
         else
             i = nextIndex(i, len);
         e = tab[i];
     }
     return null;
 }

我们已经说过 ThreadLocal 是存放在单个线程中的,每一个线程都会有自己的数据,但是 ThreadLocal 中真正的对象数据是存放在堆中的,线程对象仅仅保存了对象的引用。

当我们使用 ThreadLocal 的时候,我们通常需要在执行的方法的上下文中共享线程局部的变量。例如,我的 main 线程执行方法中的代码,但是,方法中有一段代码创建了一个新的线程,这个方法中定义的 ThreadLocal 中的变量也被这个新创建的线程使用了…这种情况下,有必要从新线程中调用外部线程的数据,这些数据需要在线程之间共享。ThreadLocal 支持父子线程共享数据这种情况。比如:

 ThreadLocal threadLocalMain = new InheritableThreadLocal();
 threadLocalMain.set("Main thread variable");
 Thread t = new Thread() {
     @Override
     public void run() {
         super.run();
         System.out.println( "The variable obtained now is = "+ threadLocalMain.get());
     }
 };
 t.start();

执行结果:

 The variable obtained now is = main thread variable

上面的代码可以实现在父线程和子线程中共享数据,关键是通过 InheritableThreadLocal 去实现共享数据。

所以,它是如何做到的呢?

下面是在 Thread 类的 init() 方法中的一小段代码:

 if (inheritThreadLocals && parent.inheritableThreadLocals != null)
     this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

这段代码意味着,当创建一个线程时,如果当前线程的 inheritThreadLocals 变量存在,并且,父线程中的 inheritThreadLocals 变量不为 null,父线程的 inheritThreadLocals 变量中的数据会赋值给当前线程中的 inheritThreadLocals 变量。

ThreadLocal 的内存泄漏问题

我们前面介绍过,Entry 类是继承了 WeakReference 类,表明了 Entry 的 key 是弱引用。

image-20220707235514670.png

弱引用类型用于描述非必须的对象。弱引用类型对象仅能存活到下一次垃圾收集之前。当垃圾收集器开始工作,不管当前内存是否充足,都会回收那些只有弱引用的对象。

弱引用依然是 ThreadLocal 对象本身,所以,通常来所,当线程执行完毕,ThreadLocal 对象会变成 null,并且弱引用对象如果为 null 就会在下一次 GC 时被清理,所以,Entry key 所占用的空间会被释放,但是 Entry 的 value 依然会占用内存,如果当前线程被重新使用(例如线程在线程池中),并且之后 ThreadLocal 不再被使用(用来从 ThreadLocalMap 获取数据),所以,ThreadLocalMap 中对应的 Entry 中的 value 会一直存在,这将会导致内存泄漏。 为了避免内存泄漏,在 ThreadLocal 使用后需要立即执行 remove() 方法去释放 key 和 value 占用的内存。

内存泄漏还是会发生,那为什么 ThreadLocal 需要设置为弱引用?

在一般情况下,对象是强引用的,但是,只要引用一直存在,强引用对象就不会被回收,所以,如果 Thread 被重用,在 Entry 中的 Key 和 Value 都不会被回收,这回导致 Key 和 value 都会占用内存。

但是,如果将 ThreadLocal 设置为弱引用,当 ThreadLocal 没有被强制引用时,它可以被回收。回收后,Entry 中的 key 变成 null。如果线程被重复使用,只要 ThreadLocal 被用来从 ThreadLocalMap 中获取数据,ThreadLocal 的 set、get 和其他方法就会被调用。当 set、get 和其他方法被调用,那些 Entry 中 key 是 null 的数据可能(get 方法:如果目标 Entry 的 key 为 null(回收了)或着发生了 Hash 冲突时,set 方法:启发式扫描。看源码)被扫描到。当发现 Entry 中有 Entry 的 key 是 null,这个 Entry 的 value 也会被设置为 null,所以,这个 Entry 的 value 也会被回收,这样可以进一步的避免内存溢出,当 ThreadLocalMap 重新 hash 的时候,也可以首先将那些 key 是 null 的 Entry 清理掉,如果 ThreadLocalMap 的空间不足,在执行扩容操作。

尽管 Entry 的 key 设置为弱引用,如果线程被重用并且 ThreadLocal 在后来的任务执行过程中不被使用,Entry 中的 value 还是占用内存,最终还是会导致内存溢出,所以,当使用 ThreadLocal 的时候,最终一定要调用 remove() 方法。

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

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

相关文章

基于鲸鱼算法的极限学习机(ELM)回归预测-附代码

基于鲸鱼算法的极限学习机(ELM)回归预测 文章目录 基于鲸鱼算法的极限学习机(ELM)回归预测1.极限学习机原理概述2.ELM学习算法3.回归问题数据处理4.基于鲸鱼算法优化的ELM5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;本文利用鲸鱼算法对极限学习机进行优化&#xff0c;并…

kettle——数据清洗(数据表——>转换——>数据表)

目录 1、表输入 ①点击“新建”选项&#xff0c;新建数据库 ②选择“获取SQL” ③选择表a ④注意&#xff1a;字段只显示了5个&#xff0c;而一共有6个字段&#xff0c;money字段需要手动添加 2、转换 ①打开java 控件&#xff0c;设置变量 3、表输出 ①连接表b ②映…

【Redis7】Redis7 复制(重点:复制原理)

【大家好&#xff0c;我是爱干饭的猿&#xff0c;本文重点介绍Redis7 复制。 后续会继续分享Redis7和其他重要知识点总结&#xff0c;如果喜欢这篇文章&#xff0c;点个赞&#x1f44d;&#xff0c;关注一下吧】 上一篇文章&#xff1a;《【Redis7】Redis7 事务&管道&…

Git入门指南(手把手教学)

Git入门指南 一、什么是Git二、Git的安装下载三、git的简单实践1.创建git仓库2.Windows上生成公钥以绑定GitHub仓库3.写一个Helloworld 四、帮助学习的网站 一、什么是Git Git是一种分布式版本控制系统&#xff0c;它是由Linus Torvalds为了管理Linux内核开发而开发的。与中心化…

项目第四天

解决了路变墙 墙变路的问题 void onechange(ExMessage* msg) {if (msg->message WM_LBUTTONDOWN && msg->x > 50 && msg->x < 410 && msg->y > 50 && msg->y < 410){//printf("鼠标位置&#xff1a;x&#…

Vue.js中class与style的增强绑定

目录 一、v-bind绑定class属性 &#xff08;1&#xff09;绑定class样式&#xff0c;字符串写法 &#xff08;2&#xff09;绑定class样式&#xff0c;数组写法 &#xff08;3&#xff09;绑定class样式&#xff0c;对象写法 二、v-bind绑定内联样式style &#xff08;1&…

【CSS3】CSS3 属性选择器 ( CSS3 简介 | 属性选择器 | 属性选择器权重 )

文章目录 一、CSS3 简介二、CSS3 属性选择器权重三、CSS3 属性选择器 一、CSS3 简介 CSS3 是在 CSS2 基础上进行扩展后的样式 ; 在 移动端 对 CSS3 的支持 要比 PC 端支持的更好 , 建议在移动端开发时 , 多使用 CSS3 ; PC 端老版本浏览器不支持 CSS3 , 尤其是 IE 9 及以下的版…

【Linux 裸机篇(五)】I.MX6ULL BSP工程管理下的 Makefile编写、链接脚本

目录 一、BSP 工程二、Makefile三、链接脚本 一、BSP 工程 文件夹描述bsp存放驱动文件imx6ul存放跟芯片有关的文件&#xff0c;比如 NXP 官方的 SDK库文件obj存放编译生成的.o 文件project存放 start.S 和 main.c 文件&#xff0c;也就是应用文件 二、Makefile 1 CROSS_COMPI…

ESP32设备驱动-BMA400加速度传感器驱动

BMA400加速度传感器驱动 文章目录 BMA400加速度传感器驱动1、BMA400介绍2、硬件准备3、软件准备4、驱动实现1、BMA400介绍 BMA400 是第一款真正的超低功耗加速度传感器,不会影响性能。 BMA400 具有 12 位数字分辨率、连续测量和定义的可选带宽以及超低功耗,允许对三个垂直轴…

带有时钟使能和同步清零的D触发器

每个 Slice 有 8 个 FF 。四个可以配置为 D 型触发器或电平敏感锁存器&#xff0c;另外四个只能配置为 D 型触发器&#xff0c;但是需要记得是&#xff1a;当原来的四个 FF 配置为锁存器时&#xff0c;不能使用这四个 FF 。 &#xff08;1&#xff09;FDCE 简介 带有时钟使能…

【grpc01】入门

目录 背景 单体架构 微服务架构 代码冗余问题 服务之间调用 grpc protobuf protoc protoc-gen-go 背景 单体架构 有一些致命缺点&#xff1a; 一旦某个服务宕机&#xff0c;会引起整个应用不可用&#xff0c;隔离性差只能整体应用进行伸缩&#xff0c;浪费资源&#…

【Java基础】迷宫问题的Java代码实现

迷宫问题通常是指在给定的迷宫中&#xff0c;找到从起点到终点的路径的问题。迷宫通常由障碍物和自由空间组成&#xff0c;其中障碍物是不可穿越的区域&#xff0c;自由空间可以穿越。解决迷宫问题的方法有很多种&#xff0c;本文使用递归算法来解决迷宫问题。 一、使用递归算法…

MATLAB简单图形绘制(五)

目录 实验目的 实验内容 实验目的 1&#xff09;掌握MATLAB图形绘制的基本原理和方法&#xff1b; 2&#xff09;熟悉和了解MATLAB图形绘制程序编辑的基本指令&#xff1b; 3&#xff09;掌握利用MATLAB图形编辑窗口编辑和修改图形界面&#xff0c;并添加图形的各种标注&…

人工智能技术在软件开发中的应用

人工智能技术的不断发展和成熟&#xff0c;使得它在软件开发中的应用越来越广泛。人工智能技术的应用可以帮助软件开发人员提高效率、降低成本、增强软件的功能性和可靠性。在本文中&#xff0c;我们将探讨人工智能技术在软件开发中的应用&#xff0c;并且提供一些实际案例&…

借助高性能计算的发展十大网络趋势

当今的网络支持复杂企业IT环境中的大量工作负载。而借助高性能计算(HPC)和人工智能/深度学习的应用程序&#xff0c;企业可以满足对更快计算周期、更高数据传输率和出色连接性日益增长的需求。 当今的网络支持复杂企业IT环境中的大量工作负载。而借助高性能计算(HPC)和人工智能…

比spire.pdf速度更快:EVO PDF Print Crack

适用于 .NET 的 EVO PDF 打印 EVO PDF Print 可用于任何类型的 .NET 应用程序&#xff0c;以静默打印 PDF 文档而不显示任何打印对话框。它可以集成到任何 .NET 应用程序中&#xff0c;包括 ASP.NET 网站和桌面应用程序&#xff0c;以便为您的应用程序添加 PDF 打印功能。您可以…

机器学习:基于心脏病数据集的XGBoost分类预测

目录 一、简介 原理&#xff1a; 二、实战演练 1.数据准备 2.数据读取/载入 3.数据预处理 4.可视化处理 5.对离散变量进行编码 6.模型训练与预测 7.特征选择 8.通过调整参数获得更好的效果 核心参数调优 网格调参法 一、简介 XGBoost&#xff08;eXtreme Gradient B…

VOS3000 AXB模块工作原理

VOS AXB 模块适用于语音市场直连运营商或虚拟运营商 X 号平台的业务需求 与 VOS 系统无缝集成&#xff0c;无需独立服务器部署&#xff0c;节约硬件&#xff0c;网络成本 单机支持不低于 2,000 并发 AXB 呼叫&#xff0c;性能是市面常见 AXB 产品的 2-3 倍 支持设定在呼叫接…

Java阶段二Day04

Java阶段二Day04 文章目录 Java阶段二Day04截至此版本可实现的流程图为V9BirdBootApplicationClientHandlerDispatcherServletHttpServletResponseHttpServletRequest V10DispatcherServletHttpServletResponseMETA-INF / mime.types V11EmptyRequestExceptionClientHandlerHtt…

使用Vue脚手架【Vue】

3. 使用 Vue 脚手架 3.1 初始化脚手架 3.1.1 说明 Vue脚手架是Vue官方提供的标准化开发工具&#xff08;开发平台&#xff09;最新的版本是4.x文档&#xff1a;https://cli.vuejs.org/zh/ 3.1.2 具体步骤 第一步&#xff08;仅第一次执行&#xff09;&#xff1a;全局安装…