Java并发编程——ThreadLocal

embedded/2025/2/25 3:27:45/

文章目录

  • 一、ThreadLocal 基本概念
  • 二、ThreadLocal 的数据结构
  • 三、GC 之后 ThreadLocal 的 key 是否为 null?
    • 3.1 Java 的四种引用类型
    • 3.2 ThreadLocal 的 key 是弱引用
    • 3.3 代码演示:GC 后 key 的状态
    • 3.4 关键点分析
    • 3.5 内存泄漏问题
  • 四、ThreadLocal.set() 方法源码详解
  • 五、ThreadLocalMap 的 Hash 算法
  • 六、ThreadLocalMap 的 Hash 冲突
  • 七、ThreadLocalMap.set()详解

一、ThreadLocal 基本概念

ThreadLocal 提供了线程局部变量的功能,即每个线程都能有一份自己的副本,这样不同线程之间的数据是互不干扰的。
线程每次访问 ThreadLocal 变量时,都会获取到自己独立的值。它的主要用途是解决多线程环境下共享数据的问题,避免了同步带来的开销。

代码示例

java">public class ThreadLocalTest {private List<String> messages = new ArrayList();public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);public static void add(String message) {holder.get().messages.add(message);}public static List<String> clear() {List<String> messages = holder.get().messages;holder.remove();System.out.println("size: " + holder.get().messages.size());return messages;}public static void main(String[] args) {ThreadLocalTest.add("hello world!");System.out.println(holder.get().messages);ThreadLocalTest.clear();}
}

输出
在这里插入图片描述

代码解析

  • ThreadLocal 初始化:holder 是一个 ThreadLocal 对象,它通过 ThreadLocal.withInitial(ThreadLocalTest::new) 初始化。这意味着每个线程首次调用 holder.get() 时,都会创建一个新的 ThreadLocalTest 实例。
  • add 方法:add 方法通过 holder.get() 获取当前线程的 ThreadLocalTest 实例,并向其 messages 列表中添加消息。
  • clear 方法:clear 方法首先获取当前线程的 messages 列表,然后调用 holder.remove() 清除当前线程的 ThreadLocal 变量。最后,它打印出 holder.get().messages.size(),由于 holder 已经被清除,所以会重新初始化一个新的 ThreadLocalTest 实例,因此 messages 列表的大小为 0。

二、ThreadLocal 的数据结构

在这里插入图片描述

1. ThreadLocalMap:每个 Thread 对象都有一个 ThreadLocal.ThreadLocalMap 类型的实例变量 threadLocals。这个 ThreadLocalMapThreadLocal 的内部类,用于存储线程局部变量。

2. ThreadLocalMap 的实现:

  • ThreadLocalMap 的实现类似于 HashMap,但它没有链表结构。它使用一个数组来存储键值对,其中键是 ThreadLocal 对象,值是线程局部变量。
  • ThreadLocalMap 中的 Entry 类继承自 WeakReference<ThreadLocal<?>>,这意味着 Entry 的键(即 ThreadLocal 对象)是弱引用。这有助于在 ThreadLocal 对象不再被使用时,垃圾回收器可以回收它,从而避免内存泄漏。

3. 线程隔离:每个线程在往 ThreadLocal 里放值的时候,都会往自己的 ThreadLocalMap 里存。读取时也是以 ThreadLocal 作为引用,在自己的 map 里找对应的键,从而实现了线程隔离。

三、GC 之后 ThreadLocal 的 key 是否为 null?

为了回答这个问题,我们需要深入理解 Java 的引用类型以及 ThreadLocal 的内部实现机制。


3.1 Java 的四种引用类型

  • 强引用
    • 最常见的引用类型,例如 Object obj = new Object()
    • 只要强引用存在,垃圾回收器永远不会回收被引用的对象,即使内存不足时。
  • 软引用
    • 使用 SoftReference 修饰的对象。
    • 在内存即将溢出时,垃圾回收器会回收软引用指向的对象。
  • 弱引用
    • 使用 WeakReference 修饰的对象。
    • 只要发生垃圾回收,若对象只被弱引用指向,就会被回收。
  • 虚引用
    • 使用 PhantomReference 修饰的对象。
    • 虚引用的唯一作用是接收对象即将被回收的通知。

3.2 ThreadLocal 的 key 是弱引用

ThreadLocalkey 是弱引用类型(WeakReference<ThreadLocal<?>>)。这意味着:

  • 如果 ThreadLocal 对象没有被强引用指向,垃圾回收器会回收该对象。
  • 如果 ThreadLocal 对象仍然被强引用指向,垃圾回收器不会回收它。

3.3 代码演示:GC 后 key 的状态

以下代码通过反射检查 GCThreadLocalkey 的状态:

java">public class ThreadLocalDemo {public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {Thread t = new Thread(()->test("abc",false));t.start();t.join();System.out.println("--gc后--");Thread t2 = new Thread(() -> test("def", true));t2.start();t2.join();}private static void test(String s,boolean isGC)  {try {new ThreadLocal<>().set(s);if (isGC) {System.gc();}Thread t = Thread.currentThread();Class<? extends Thread> clz = t.getClass();Field field = clz.getDeclaredField("threadLocals");field.setAccessible(true);Object ThreadLocalMap = field.get(t);Class<?> tlmClass = ThreadLocalMap.getClass();Field tableField = tlmClass.getDeclaredField("table");tableField.setAccessible(true);Object[] arr = (Object[]) tableField.get(ThreadLocalMap);for (Object o : arr) {if (o != null) {Class<?> entryClass = o.getClass();Field valueField = entryClass.getDeclaredField("value");Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");valueField.setAccessible(true);referenceField.setAccessible(true);System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));}}} catch (Exception e) {e.printStackTrace();}}
}

运行结果:
在这里插入图片描述
在这里插入图片描述
如图所示,因为这里创建的ThreadLocal并没有指向任何值,也就是没有任何引用:

java">new ThreadLocal<>().set(s);

所以这里在GC之后,key就会被回收,我们看到上面debug中的referent=null, 如果改动一下代码:
在这里插入图片描述


3.4 关键点分析

在这里插入图片描述

  • 为什么 key 会被回收?
    • 代码中创建的 ThreadLocal 对象是局部变量,没有强引用指向它。
    • 当触发 GC 时,ThreadLocal 对象只被弱引用指向,因此被回收,key 变为 null
  • 为什么值未被回收?
    • ThreadLocalMapEntry 中,key 是弱引用,但 value 是强引用。
    • 如果 key 被回收,value 仍然存在,导致内存泄漏。
  • ThreadLocal.get()key 是否为 null
    • 如果 ThreadLocal 对象仍然被强引用指向,key 不会为 null
    • 如果 ThreadLocal 对象没有被强引用指向,key 可能被回收,变为 null

3.5 内存泄漏问题

  • 原因ThreadLocalMapEntry 中,key 是弱引用,value 是强引用。
    如果 key 被回收,value 仍然存在,且无法通过 ThreadLocal 访问,导致内存泄漏。
  • 解决方案:使用完 ThreadLocal 后,调用 remove() 方法清除 Entry,避免内存泄漏。

四、ThreadLocal.set() 方法源码详解

ThreadLocal 中的 set 方法用于将当前线程的局部变量设置为指定的值。其核心逻辑是判断 ThreadLocalMap 是否存在,然后根据情况进行相应的处理。下面我们逐步解析 set 方法的源码及其背后的原理。

1. set 方法源码

java">public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}

在这里插入图片描述

ThreadLocal中的set方法原理如上图所示,很简单,主要是判断ThreadLocalMap是否存在,然后使用ThreadLocal中的set方法进行数据处理。

五、ThreadLocalMap 的 Hash 算法

1. 基本原理
ThreadLocalMap 作为一种 Map 结构,需要实现自己的 hash 算法来解决散列表数组冲突问题。其 hash 算法核心代码为

java">int i = key.threadLocalHashCode & (len - 1);

其中,i 就是当前 key 在散列表中对应的数组下标位置。

2. threadLocalHashCode 值的计算
threadLocalHashCode 值的计算在 ThreadLocal 类中完成,相关代码如下:

java">public class ThreadLocal<T> {private final int threadLocalHashCode = nextHashCode();private static AtomicInteger nextHashCode = new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}static class 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);}}
}

每当创建一个 ThreadLocal 对象,ThreadLocal.nextHashCode 这个值就会增长 0x61c88647。这个值是斐波那契数(黄金分割数),使用它作为 hash 增量,能让 hash 分布非常均匀。

六、ThreadLocalMap 的 Hash 冲突

注明: 下面所有示例图中,绿色块Entry代表正常数据,灰色块代表Entry的key值为null,已被垃圾回收。白色块表示Entry为null。

1. 不同的解决方式

  • HashMap:使用数组 + 链表(或红黑树)的方式解决冲突,冲突的数据挂载到链表上,当链表长度超过一定数量则会转化成红黑树。
  • ThreadLocalMap:没有链表结构,不能采用 HashMap 解决冲突的方式。

2. 线性探测法
当插入数据时,如果通过 hash 计算得到的槽位已经有 Entry 数据,就会线性向后查找,一直找到 Entrynull 的槽位才会停止查找,并将当前元素放入此槽位中。
在这里插入图片描述

如上图所示,如果我们插入一个value=27的数据,通过hash计算后应该落入第4个槽位中,而槽位4已经有了Entry数据。此时就会线性向后查找,一直找到Entry为null的槽位才会停止查找,将当前元素放入此槽位中。

3. 特殊情况处理
在迭代过程中,会遇到不同情况,如 Entry 不为 nullkey 值相等的情况,还有 Entry 中的 key 值为 null 的情况等,会有不同的处理方式。由于 Entry 中的 key 是弱引用类型,可能会存在 key 过期的 Entry 数据。在 set 过程中,如果遇到了 key 过期的 Entry 数据,实际上会进行一轮探测式清理操作。

七、ThreadLocalMap.set()详解

看完了ThreadLocal hash算法后,我们再来看set是如何实现的。

ThreadLocalMapset数据(新增或者更新数据)分为好几种情况,针对不同的情况我们画图来说说明。

第一种情况: 通过hash计算后的槽位对应的Entry数据为空:
在这里插入图片描述

直接将数据放到该槽位即可

第二种情况: 槽位数据不为空,key值与当前ThreadLocal通过hash计算获取的key值一致:
在这里插入图片描述

直接更新该槽位的数据。

第三种情况: 槽位数据不为空,往后遍历过程中,在找到Entry为null的槽位之前,没有遇到key过期的Entry:
在这里插入图片描述

遍历散列数组,线性往后查找,如果找到Entry为null的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了key值相等的数据,直接更新即可。

第四种情况: 槽位数据不为空,往后遍历过程中,在找到Entry为null的槽位之前,遇到key过期的Entry,如下图,往后遍历过程中,一到了index=7的槽位数据Entry的key=null:
在这里插入图片描述

散列数组下标为7位置对应的Entry数据key为null,表明此数据key值已经被垃圾回收掉了,此时就会执行replaceStaleEntry()方法,该方法含义是替换过期数据的逻辑,以index=7位起点开始遍历,进行探测式数据清理工作。

具体步骤如下:

1. 初始化探测式清理过期数据扫描的开始位置
设置 slotToExpunge = staleSlot = 过期槽位的下标。例如,当遇到 index = 7 的槽位数据 Entry 的 key = null 时,staleSlot = 7slotToExpunge 初始也为 7。

2. 以当前 staleSlot 开始向前迭代查找
通过 for 循环迭代,从 staleSlot 位置向前查找其他过期的数据,并更新过期数据起始扫描下标 slotToExpunge,直到碰到 Entrynull 时结束迭代。例如,在向前迭代过程中,如果又找到了其他过期的数据,会继续向前查找,直到遇到 Entry = null 的槽位才停止,此时 slotToExpunge 会被更新为新的过期元素的下标。这个操作的目的是为了确定当前过期槽位 staleSlot 之前是否还有过期元素。
如下图所示,slotToExpunge被更新为0:
在这里插入图片描述
3. 接着以 staleSlot 位置向后迭代

  • 若找到了相同 key 值的 Entry 数据:从当前节点 staleSlot 向后查找 key 值相等的 Entry 元素,找到后更新该 Entry 的值,并交换 staleSlot 元素的位置(因为 staleSlot 位置为过期元素),然后开始进行过期 Entry 的清理工作。
    在这里插入图片描述

  • 若没有找到相同 key 值的 Entry 数据:从当前节点 staleSlot 向后查找 key 值相等的 Entry 元素,直到遇到 Entry 为 null 则停止寻找。若此时 table 中没有 key 值相同的 Entry,则创建新的 Entry,替换 table[staleSlot] 位置的数据。
    在这里插入图片描述
    在这里插入图片描述

4. 过期元素清理工作
无论是更新 Entry 后还是替换 Entry 后,都会进行过期元素清理工作。清理工作主要涉及两个方法:expungeStaleEntry()cleanSomeSlots(),具体细节后续会进一步讲解。

在这里插入图片描述


http://www.ppmy.cn/embedded/164944.html

相关文章

【MySQL篇】数据库基础

目录 1&#xff0c;什么是数据库&#xff1f; 2&#xff0c;主流数据库 3&#xff0c;MySQL介绍 1&#xff0c;MySQL架构 2&#xff0c;SQL分类 3&#xff0c;MySQL存储引擎 1&#xff0c;什么是数据库&#xff1f; 数据库&#xff08;Database&#xff0c;简称DB&#xf…

百度首页上线 DeepSeek 入口,免费使用

大家好&#xff0c;我是小悟。 百度首页正式上线了 DeepSeek 入口&#xff0c;这一重磅消息瞬间在技术圈掀起了惊涛骇浪&#xff0c;各大平台都被刷爆了屏。 百度这次可太给力了&#xff0c;PC 端开放仅 1 小时&#xff0c;就有超千万人涌入体验。这速度&#xff0c;简直比火…

力扣hot100 ——搜索二维矩阵 || m+n复杂度优化解法

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。每列的元素从上到下升序排列。 解题思路&#xff1a; 借助行和列有序特性&#xff0c;不断按行或者列缩小范围&#xff1b;途中数字表示每…

k8s Container runtime network not ready

问题 k8s 3 控制节点,docker 运行时,后期踢掉其中一个节点,使用了 containerd 运行时,但是在加入集群的时候,node 状态 notready。查看 kubelet 的日志发现如下报错 Feb 20 11:28:14 bjm3 kubelet[144781]: E0220 11:28:14.506374 144781 kubelet.go:2475] "Conta…

23种设计模式之《桥接模式(Bridge)》在c#中的应用及理解

程序设计中的主要设计模式通常分为三大类&#xff0c;共23种&#xff1a; 1. 创建型模式&#xff08;Creational Patterns&#xff09; 单例模式&#xff08;Singleton&#xff09;&#xff1a;确保一个类只有一个实例&#xff0c;并提供全局访问点。 工厂方法模式&#xff0…

设计模式相关知识点

目录 设计模式 设计模式 代码设计原则 设计模式 设计模式 干掉if...else&#xff0c;最好用的3种设计模式&#xff01; | 小傅哥 bugstack 虫洞栈 代码设计原则-CSDN博客 23种设计模式-CSDN博客 策略模式&#xff08;Strategy Pattern&#xff09;-CSDN博客 责任链模式…

C从入门到放弃篇1

各位新入坑C语言的朋友&#xff0c;你们有福了因为你们遇到了我&#xff0c;我会带你放弃C语言&#xff0c;哈哈哈哈哈。 其实&#xff0c;学任何东西都是循序渐进的&#xff0c;在学习的初期投入更多的精力&#xff0c;将来你会越学越快。我相信&#xff0c;放弃是最容易的事…

浏览器下载vue.js.devtools,谷歌浏览器和edg浏览器

1、谷歌浏览器下载&#xff1a; 情况一&#xff1a;如果谷歌应用商店可以打开&#xff0c;那么就直接到谷歌应用商店下载&#xff0c;直接搜索vue.js.devtools添加扩展即可。 情况二&#xff1a;谷歌浏览器的谷歌应用商城打不开&#xff0c;那么就百度搜索极简插件找到vue.js.…