ThreadLocal这点牛角尖总算是给我钻明白了

前言

这个问题算是我的一个羞耻点,起源于一次面试中,面试官问ThreadLocal的底层实现是啥,我那时候一直以为ThreadLocal是一个类似于Redis一样的独立于线程外的第三方存储容器,如何底层维护了一个Map结构,以线程ID为Key,存储的数据为Value,通过线程id去找到这个存储值,那时候还很坚定这个想法,最后面试官说:em~你再回去了解下吧。我:⊙ˍ⊙

ThreadLocal 底层原理

ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享,可以把ThreadLocal理解为商场的寄存柜,一个人只能有一个柜口,但是可以有多个寄存柜 (为啥这样子比喻?看下去~)

set()源码

scss

复制代码

public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { // 当前线程自身所绑定的ThreadLocal对象 return t.threadLocals; }

可以看到它调用了Thread的 threadLocals 对象:

java

复制代码

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

到这里我们基本可以知道ThreadLocal数据隔离的真相了,每个线程Thread都维护了自己的 ThreadLocalMap 成员变量,所以每次使用ThreadLocal.get()时都是从自己线程里面拿到自己的ThreadLocalMap 变量,拿不到别人的变量,从而实现了数据隔离。由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。

javascript

复制代码

// 第一次执行set操作需要调用createMap函数初始化线程内部的ThreadLocal void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

在ThreadLocalMap的构造方法里,蕴含着初始化创建table数组 (封装类Entry的数组集合,Entry封装了当前的ThreadLocal对象和要存放的值) 的逻辑,源码和注释如下所示:

是不是迷惑为什么ThreadLocalMap中需要存放的是个数组,不是说好了同一个ThreadLocal中只能找到一个位置存一个值吗?

答:一个线程只能在一个ThreadLocal中存放一个值没错。但是现在的问题是我ThreadLocal又不是只有一个,不要把ThreadLocal当作一个第三方缓存啊。可以把ThreadLocal理解为商场的寄存柜,一个人只能有一个柜口;商场那么多,你可以在多个商场的寄存柜中各自存放一件商品,对于你自身来说,你只需要做好记录,说我在哪个商场存放过东西就行。

ThreadLocal——商场的寄存柜 当前线程Thread——你 Thread.ThreadLocalMap——存放记录清单,里面维护了个数组,支持记录多个商场的寄存柜地址,你用一个ThreadLocal来调用set / get 方法,本质上就是校验清单中寄存柜地址并执行相关操作

【解释】从上面源码中我们可以看到,数组默认大小是 16 ,设定的阈值为 0.75 倍的数组长度,并且根据传入的参数,创建了table数组中的第一个Entry元素对象 (ThreadLocal对象为Key, 数据值为Value) 。其中,size用来记录数组中存在的Entry元素的个数。

set插入操作其实很简单,大概流程看看:

get方法

这个其实没啥好说的,就是通过你调用的这个ThreadLocal对象去找当前线程中内置的ThreadLocalMap清单,找到该ThreadLocal对象通过Hash后得到的对应的那个数组下标提取数据就行,如果有

ThreadLocal-哈希冲突

ThreadLocalMap 使用开放定址法解决冲突,具体来说是线性探测法(linear probing) 。线性探测法会在发生哈希冲突时,依次检查下一个位置,直到找到可用的槽位存储数据。

问:线性探测法通过后移寻找空闲位置来插入发生哈希冲突的值,那这样子的话该空闲位置不就被占用了吗?如果下一次有个元素hash后刚好到这个位置咋办?

答:在这种情况下,线性探测法会继续往后查找下一个空闲位置,并依次检查是否有其他元素占用。这个过程被称为"探测"。如果找到了下一个空闲位置,则将元素插入到该位置。如果整个哈希表都被占满了(即没有空闲位置),则说明哈希表已满,无法再插入新的元素。如果哈希表发生冲突较多,可以考虑使用其他的解决冲突的方法,如链式寻址法或开放定址法的二次探测、双重散列等。

为什么要采用线性探测法不用链式寻址法

  • 线性探测法比较适用于哈希表大小相对较小的情况,而 ThreadLocal 的规模通常比较小。因为每个线程都需要独立的空间,所以使用链式寻址法的方式,在开销和效率方面不如线性探测法。
  • 线性探测法具有较好的局部性和缓存友好性,可以使 CPU 缓存的效率得到优化,提高程序的执行效率。

ThreadLocal-内存泄露

  • 强引用: 最为普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则GC并不会回收它。即便堆中内存不足了,宁可出现OOM,也不会对其进行回收
  • 弱引用: 表示一个对象处于可能有用且非必须的状态。在GC线程扫描内存区域时,一旦发现弱引用,就会回收到弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够,也就是说**被弱引用关联的对象只能存活到下一次垃圾回收发生之前。**

每一个Thread维护一个ThreadLocalMap,在ThreadLocalMap中的Entry对象继承了WeakReference。其中key为使用弱引用的ThreadLocal实例,value为强引用的线程变量副本

一旦Key被回收了,key 的引用就变成了 null,就会导致这个内存永远无法被访问,造成内存泄漏。

问题:如果这个线程被回收了,那线程里面的成员变量不是都会被回收吗?就不会存在内存泄漏问题啊?

答:在实际应用中,我们一般都是使用线程池,而线程池本身是重复利用的,所以还是会存在内存泄漏的问题。

解决方法

ThreadLocal自救:

  • ThreadLocal每次调用 get、set、remove 等方法时,会顺便检查并清理掉 Entry 中 Key 为 null 的数据。

伟大的shi山创造者干涉:

  • 每次使用完 ThreadLocal 以后,主动调用 remove()方法移除数据
  • 把 ThreadLocal 声明称全局变量,使得它无法被回收

ThreadLocal 使用场景

  • 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束
  • 线程间数据隔离
  • 进行事务操作,用于存储线程事务信息。
  • 数据库连接,Session会话管理。

Spring框架在事务开始时会给当前线程绑定一个Jdbc connection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离

<think>嗯,用户想了解ThreadLocal的使用方法和示例代码。首先,我需要回忆一下ThreadLocal的基本概念。ThreadLocalJava中用来提供线程局部变量的类,每个线程都可以独立地访问自己的变量副本,不会和其他线程的副本冲突。这通常用于解决多线程环境中的变量共享问题。根据用户提供的引用,比如引用[1]提到ThreadLocal是一个有趣且实用的概念,很多开发者不知道怎么用。所以需要先从基础开始解释。引用[2]和[3]给出了代码示例,说明如何设置、获取和移除ThreadLocal变量。还有引用[4]中的TestContext类,展示了如何结合具体场景使用ThreadLocal。 用户可能需要一个结构清晰的教程,分步骤讲解使用方法。应该包括创建ThreadLocal对象、设置值、获取值、移除值以及初始值的方法。同时,需要给出代码示例,帮助用户理解。比如引用[3]中的示例代码展示了普通变量和线程局部变量的区别,可以引用这个例子来说明线程安全的问题。 还需要注意可能的问题,比如内存泄漏,引用[1]提到示例代码不适合生产环境,用户可能需要了解如何正确使用remove方法避免内存泄漏。此外,初始化的方法,比如重写initialValue,也是关键点,引用[3]中的示例有提到。用户可能不知道ThreadLocal的应用场景,可以结合引用中的例子,比如保存线程上下文信息,如用户会话、数据库连接等,这样在不同方法中传递参数更方便。需要给出实际的应用场景示例,比如引用[4]中的TestContext类,用来保存测试上下文信息。 最后,生成相关问题,帮助用户进一步学习。比如如何避免内存泄漏,父子线程如何共享变量,或者与其他线程安全机制的比较。这些问题可以引导用户深入理解ThreadLocal的使用和注意事项。</think>### ThreadLocal使用教程及示例代码 ThreadLocalJava中用于存储线程私有变量的工具类,可解决多线程环境下变量共享冲突问题。以下是详细使用步骤: #### 一、基础使用步骤 1. **创建ThreadLocal对象** ```java private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); ``` 2. **设置线程局部变量** ```java threadLocal.set("当前线程值"); // 每个线程独立存储 ``` 3. **获取线程局部变量** ```java String value = threadLocal.get(); // 返回当前线程存储的值 ``` 4. **移除变量(避免内存泄漏)** ```java threadLocal.remove(); // 线程结束时必须调用[^3] ``` #### 二、初始化方法 通过重写`initialValue()`设置默认值: ```java private static ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> 0); // 或 private static ThreadLocal<Integer> counter = new ThreadLocal<>() { @Override protected Integer initialValue() { return 0; } }; ``` #### 三、完整示例代码 ```java public class ThreadLocalDemo { // 定义ThreadLocal变量并初始化 private static ThreadLocal<Integer> threadCounter = ThreadLocal.withInitial(() -> 0); public static void main(String[] args) { // 创建三个线程 for (int i = 0; i < 3; i++) { new Thread(() -> { // 获取当前线程的计数器值 int count = threadCounter.get(); // 修改并存储新值 threadCounter.set(count + 1); System.out.println(Thread.currentThread().getName() + " 的计数器值: " + threadCounter.get()); // 必须清理 threadCounter.remove(); }).start(); } } } ``` 输出结果: ``` Thread-0 的计数器值: 1 Thread-1 的计数器值: 1 Thread-2 的计数器值: 1 ``` #### 四、典型应用场景 1. **线程上下文传递**(如用户会话、事务ID) ```java // 上下文管理类 public class UserContext { private static ThreadLocal<User> currentUser = new ThreadLocal<>(); public static void set(User user) { currentUser.set(user); } public static User get() { return currentUser.get(); } public static void clear() { currentUser.remove(); } } ``` 2. **线程安全工具类**(如SimpleDateFormat) ```java private static ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); ``` #### 五、注意事项 1. **必须调用remove()**:线程池场景下不清理会导致内存泄漏和旧数据残留[^3] 2. **避免继承问题**:子线程默认无法获取父线程的ThreadLocal值(需使用InheritableThreadLocal) 3. **空值处理**:未设置值时get()返回null,建议使用initialValue初始化
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值