ThreadLocal 线程变量
ThreadLocal 是 Java 中用于实现线程局部变量的工具类,它为每个使用该变量的线程都提供一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。
共享实例 线程隔离:
java
/**
* threadLocal 线程变量
* 多线程使用的是同一个实例的 threadLocal 但是每个线程的 threadLocal 是独立的
* 每个线程又有自己的线程副本
*
* 共享实例,线程隔离
*/
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void print(String str){ //参数:线程名字
System.out.println("str: " + threadLocal.get());
threadLocal.remove();
}
@Test
public void testThreadLocal() throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("thread1");
print("thread1");
System.out.println("表明同一个threadLocal 实例 = " + threadLocal);
System.out.println("删除线程变量后:threadLocal1 = " + threadLocal.get());
}
}).start();
Thread.sleep(1000);
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("thread2");
print("thread2");
System.out.println("表明同一个threadLocal 实例 " + threadLocal);
System.out.println("删除线程变量后:threadLocal2 = " + threadLocal.get());
}
}).start();
}
ThreadLocal 的 set 方法
java
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程
ThreadLocalMap map = getMap(t);
if (map != null) //判断当前线程中的 ThreadLocalMap属性 是否为空
map.set(this, value); //如果 map 存在,以当前 ThreadLocal 实例为键,存储 value
//这里的 this 指的是当前 ThreadLocal 实例,它作为键存储在 ThreadLocalMap 中。
else
createMap(t, value);//如果 map 不存在,创建一个新的 ThreadLocalMap
}
java
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; // ThreadLocal.ThreadLocalMap threadLocals = null;
}
流程:
kotlin
set(value)
├── 获取当前线程 t
├── 获取 t 的 threadLocals
│ ├── 若存在 → 调用 map.set(this, value)
│ │ ├── 计算索引 i
│ │ ├── 遍历数组
│ │ │ ├── 找到键 → 更新值
│ │ │ ├── 找到 stale 槽 → 替换并清理
│ │ │ └── 未找到 → 创建新 Entry
│ │ └── 检查是否需要扩容
│ └── 若不存在 → 创建新的 ThreadLocalMap
└── 结束
ThreadLocal 的 get 方法
java
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的 ThreadLcoalMap
ThreadLocalMap map = getMap(t);
// 3.判断map是否存在
if (map != null) {
//以当前 ThreadLocal 实例为键,查找 Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
java
private T setInitialValue() {
T value = initialValue(); //null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
流程:
kotlin
get()
├── 获取当前线程 t
├── 获取 t 的 threadLocals
│ ├── 若存在 → 调用 map.getEntry(this)
│ │ ├── 计算索引 i
│ │ ├── 检查表[i]
│ │ │ ├── 找到键 → 返回值
│ │ │ ├── 键为 null → 清理并继续查找
│ │ │ └── 未找到 → 继续线性探测
│ │ └── 未找到键 → 返回 null
│ └── 若不存在 → 创建新的 ThreadLocalMap 并设置初始值
└── 返回值(或初始值)
ThreadLocal 的 remove 方法
ThreadLocal
的 remove()
方法用于从当前线程的 ThreadLocalMap
中移除该 ThreadLocal
实例对应的值。这是避免内存泄漏的关键操作
java
public void remove() {
//同样获取当前线程的 ,然后获取当前线程的 ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
remove() 方法实现
java
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 1. 计算索引
int i = key.threadLocalHashCode & (len-1);
// 2. 线性探测查找目标键
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 3. 找到目标键后,调用 WeakReference 的 clear() 方法
e.clear();
// 4. 清理过期条目(防止内存泄漏)
expungeStaleEntry(i);
return;
}
}
}
为了防止内存泄露,我们必须要remove
scss
在 ThreadLocal 的生命周期中,如果没有手动调用 remove(),可能会导致以下问题:
- 当 ThreadLocal 实例的外部强引用被回收后,其在 ThreadLocalMap 中的弱引用键会变为 null。
- 但如果线程(如线程池中的线程)长期存活,ThreadLocalMap 中的值可能不会被回收,造成内存泄漏。
- 调用 remove() 可以主动清理这些过期条目,避免内存泄漏。
流程:
scss
remove()
├── 获取当前线程 t
├── 获取 t 的 threadLocals
│ ├── 若存在 → 调用 map.remove(this)
│ │ ├── 计算索引 i
│ │ ├── 线性探测查找键
│ │ │ ├── 找到键 → 调用 e.clear()
│ │ │ └── 调用 expungeStaleEntry(i) 清理
│ └── 若不存在 → 无操作
└── 结束
ThreadLocal 的 实战使用
快递分拣中心的编号标签管理
场景描述:
你是一个在快递分拣中心工作的工作人员,每个分拣员(线程)负责将快递包裹(任务)按目的地分类,为了方便追踪,每个包裹需要粘贴一个 唯一编号标签 (如:"分拣员1-001","分拣员2-001")。
如果没有 ThreadLocal ,所有分拣员共用一个编号本的话,就有可能造成编号混乱的情况出现,例如:
-
分拣员1:给包裹贴了"分拣员1-001",但是编号本被分拣员2误操作,导致编号重复或跳号。
-
分拣员2:在分拣时,不小心使用了分拣员1 的编号,造成数据错误。
1. 问题:共享资源导致的编号混乱
-
如果没有
ThreadLocal
,所有分拣员共用一个编号生成器(如AtomicInteger
),会出现以下问题: -
- 线程安全问题:多个线程同时修改共享变量,可能导致数据不一致。
- 编号冲突:分拣员无法快速生成专属编号,效率低下。
2. 解决方案:用 ThreadLocal 为每个线程分配独立的编号本
-
通过
ThreadLocal
,每个线程(分拣员)都有自己的AtomicInteger
(编号本),独立生成编号。这样: -
- 分拣员1 的编号本只用于自己,生成"分拣员1-001"、"分拣员1-002"。
代码实现示例:
java
//定义 ThreadLocal ,为每个线程分配独立的编号本
// 定义 ThreadLocal,为每个线程分配独立的编号本(AtomicInteger)
public class SortingContext {
private static final ThreadLocal<AtomicInteger> counter = ThreadLocal.withInitial(() -> new AtomicInteger(1));
// 获取当前线程的编号本,并生成下一个编号
public static String generateLabel() {
return Thread.currentThread().getName() + "-" + counter.getAndIncrement();
}
// 清理资源(避免内存泄漏)
public static void clear() {
counter.remove();
}
}
// 模拟分拣员(线程)处理包裹
public class Sorter implements Runnable {
@Override
public void run() {
try {
// 1. 分拣员开始分拣包裹,生成编号
for (int i = 0; i < 3; i++) {
String label = SortingContext.generateLabel();
System.out.println(label + " 的包裹已贴上标签");
}
} finally {
// 2. 分拣结束后,清理编号本(避免内存泄漏)
SortingContext.clear();
}
}
}
// 启动多个分拣员线程
public class SortingCenter {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
Thread sorter = new Thread(new Sorter(), "分拣员-" + i);
sorter.start();
}
}
}
扩展:
1️⃣:如何理解 当 ThreadLocal 实例的外部强引用被回收后,其在 ThreadLocalMap
中的弱引用键会变为 null。
在Java 中,对象的引用分为不同的引用类型,其中最常见的是强引用和弱引用
-
强引用
最常见的引用类型,例如:
Object obi = new Object();
只要强引用存在,对象就不会被垃圾回收。 -
弱引用
通过
WeakReference
类实现,当一个对象仅被弱引用指向时,无论内存是否存在,该对象都会在下一次垃圾回收时被回收。javaObject strongRef = new Object(); // 强引用 WeakReference<Object> weakRef = new WeakReference<>(strongRef); // 弱引用指向同一个对象 strongRef = null; // 断开强引用 // 此时对象仅被 weakRef 弱引用指向,下一次 GC 时会被回收
ThreadLocal
的数据存储在每个线程的 ThreadLocalMap
中,而 ThreadLocalMap
的键是 WeakReference<ThreadLocal<?>>:
java
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 弱引用指向 ThreadLocal 实例
value = v;
}
}
ThreadLocalMap
的键是对 ThreadLocal 实例的弱引用,而值是用户存储的对象(强引用)。
java
public class MyClass {
// 静态变量是强引用,指向 ThreadLocal 实例
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set(123); // 存储值到当前线程的 ThreadLocalMap
// 假设后续代码中,断开了对 threadLocal 的强引用
threadLocal = null;
}
}
此时内存中的状态:
- 外部强引用消失:
threadLocal = null
后,没有任何强引用指向ThreadLocal
实例。 - 弱引用仍然存在:
ThreadLocalMap
的键(Entry)
通过弱引用指向ThreadLocal
实例。
当垃圾回收发生时:
ThreadLocal
实例被回收:由于没有强引用,仅存的弱引用无法阻止GC
回收该实例。- 弱引用键变为
null:ThreadLocal
实例被回收后,Entry
中的弱引用get()
方法返回null
。
流程:
ini
// 初始状态
[主线程]
↳ static threadLocal → ThreadLocal@1234 ← 弱引用 [ThreadLocalMap 的 Entry]
↳ value = 123
// 执行 threadLocal = null 后
[主线程]
↳ static threadLocal = null
[ThreadLocalMap 的 Entry]
↳ 弱引用 → null (ThreadLocal@1234 已被 GC 回收)
↳ value = 123 (强引用仍然存在)
弱引用管键(ThreadLocal 实例),强引用管值(用户数据)
正是因为,我们的key :threadLocal
是弱引用,在key 被垃圾回收之后,就会出现key 为 null 的value ,如果不清理,就会造成内存泄露。所以我们要及时 remove 。
2️⃣: Thread
, ThreadLocal
,ThreadLocalMap
三者之间的关系
Thread : (线程)
- 线程是操作系统能够进行运算调度的最小单位,进程包括线程。一个进程包括多个线程。
ThreadLocal : (线程局部变量)
- 为每个线程提供一个独立的变量副本,每个线程拥有可以独立改变自己的副本,而不会影响其他线程的变量副本。
ThreadLocalMap : (线程局部变量映射表)
- ThreadLocalMap 是 ThreadLocal 的静态内部类,每个 Thread 对象都包含一个 ThreadLocalMap 成员变量(threadLocals)。它用于存储线程的局部变量,键为 ThreadLocal 实例,值为用户存储的对象。弱引用键 :键(
ThreadLocal
实例)是弱引用,值是强引用。
关系:
每个线程都有自己的 一个 ThreadLocalMap
(哈希表) 属性,键为 ThreadLocal
对象实例,值为线程对应的变量副本的值。一个线程 可以绑定多个 ThreadLocal 实例,每个 ThreadLocal
都会作为 ThreadLocalMap
的键(Key),与对应的值(Value)一起存储在同一个 ThreadLocalMap
中。每个线程(Thread
)内部维护一个 ThreadLocalMap
对象(threadLocals
字段),用于存储该线程的所有 ThreadLocal
变量及其对应的值。
角色 | 核心功能 | 隔离性 | 生命周期 |
---|---|---|---|
Thread |
执行单元 | 每个线程独立 | 线程创建到销毁 |
ThreadLocal |
提供线程局部变量的访问接口 | 线程间数据隔离 | 由外部引用控制 |
ThreadLocalMap |
存储线程局部变量 | 每个线程独立一份 | 随线程生命周期变化 |
例子:
- 假设你在一家公司上班,每个**员工(线程)**每天需要记录自己的工作任务。
- 公司规定:每人必须有一本独立的笔记本(不能共享),用来记录自己的待办事项。
- 这本笔记本(ThreadLocal)有个特性:只有你自己能打开并修改里面的内容,其他同事看不到也改不了。
- 而公司会给每个员工分配一个专属的文件柜(ThreadLocalMap) ,用来存放这本笔记本和其他私人物品。