本文将详细介绍Java并发编程(多线程)中常见的 ThreadLocal 类以及如何去使用它。
ThreadLocal类
1、ThreadLocal
是 Java 中提供的一种机制,用于为每个线程提供独立的变量副本,确保每个线程都有自己的变量,而不会和其他线程共享这些变量(即每个线程都有独立的存储空间,线程之间互不干扰。)
用途:主要用于解决多线程并发访问时的线程安全问题,尤其是在不想使用复杂的锁机制时,它是非常有效的工具。
2、创建方式:
2.1 最基础的:
ThreadLocal tl = new ThreadLocal();
这时:
-
没有初始值;
-
第一次调用 get() 会返回 null(除非重写
initialValue()
)ThreadLocal<String> local = new ThreadLocal<String>() { @Override protected String initialValue() { return "默认值"; } };
2.2 推荐方式:这个本质上也是创建一个 ThreadLocal
实例。
ThreadLocal<String> local = ThreadLocal.withInitial(() -> "默认值");
3、常用方法:
get()
:
- 获取当前线程的
ThreadLocal
变量值。 - 如果该线程是第一次调用
get()
,则会返回通过initialValue()
或withInitial()
方法指定的初始值。
set(T value)
:
- 设置当前线程的
ThreadLocal
变量值。
remove()
:
- 移除当前线程的
ThreadLocal
变量,清除与线程绑定的值,释放内存。
initialValue()
:
- 为当前线程提供初始值,当线程第一次调用
get()
时,如果没有通过set()
设置值,就会调用initialValue()
。
withInitial(Supplier<? extends T>)
:
- Java 8 引入的工厂方法,用来简化
ThreadLocal
的初始化过程。它接受一个Supplier
接口,提供初始化值。
4、示例:
public class Test {
// 创建一个 ThreadLocal 用于存储每个线程的账户余额:初始余额 100
private static ThreadLocal<Integer> balance = ThreadLocal.withInitial(() -> 100);
public void withdraw(int amount) {
balance.set(balance.get() - amount); // 每个线程更新自己的余额
}
public int getBalance() {
return balance.get(); // 获取当前线程的余额
}
public static void main(String[] args) {
BankAccount account = new BankAccount();
// 启动两个线程,分别操作账户
Thread t1 = new Thread(() -> {
account.withdraw(50);
System.out.println(account.getBalance()); // 输出: 50
});
Thread t2 = new Thread(() -> {
account.withdraw(30);
System.out.println(account.getBalance()); // 输出: 70
});
t1.start();
t2.start();
}
}
注意:ThreadLocal
使用不当可能会导致内存泄漏。因为线程池中的线程通常是复用的,而 ThreadLocal
绑定的是线程私有数据。如果不调用 remove()
方法清除 ThreadLocal
变量,可能会导致线程长期持有该变量,无法释放内存。因此,建议在使用完 ThreadLocal
后调用 remove()
方法来清理资源。
ThreadLocal底层原理
每个线程 Thread
内部有一个 ThreadLocalMap
,键是 ThreadLocal
对象本身,值是当前线程中存储的变量副本:
// ThreadLocalMap 的构造函数(源码:)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY]; // 默认容量是16
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
}
Thread.currentThread()
└── threadLocals(ThreadLocalMap)
└── table[i] = Entry(WeakReference<ThreadLocalA>, valueA)
└── table[i+1] = Entry(WeakReference<ThreadLocalB>, valueB)
简单流程:
ThreadLocal<String> local = new ThreadLocal<>();
local.set("abc");
String value = local.get();
- 调用
local.set(value)
,其实是:- 获取当前线程(
Thread.currentThread()
); - 拿到当前线程的
ThreadLocalMap
(也就是Thread.threadLocals
),没有就创建; - 以 local 为 key,value 为你传的对象,存进
ThreadLocalMap
- 获取当前线程(
- 调用
local.get()
,其实是:- 当前线程找自己的
ThreadLocalMap
; - 拿 local 这个 key 对应的 value
- 当前线程找自己的
每个线程互不影响,不用加锁也线程安全!
1. ThreadLocalMap是什么
1、ThreadLocalMap
是 ThreadLocal
的一个静态内部类,它不是 Java 标准库中的 Map
接口的实现(如 HashMap 或 ConcurrentHashMap),而是专 为 ThreadLocal
服务的一种自定义轻量级 Map。
注意: 但 ThreadLocalMap 的实例却保存在 Thread 对象里,可以看 Thread 源码:
// Thread.java(源码简化)
public class Thread implements Runnable {
// 每个线程拥有一个存储 ThreadLocal 数据的 map(也就是ThreadLocalMap)
ThreadLocal.ThreadLocalMap threadLocals = null;
}
2、怎么获取 ThreadLocalMap
?以 ThreadLocal.set()
为例。从源码角度:看是如何获取 ThreadLocalMap
① 看 ThreadLocal.set()
源码:
// ThreadLocal.java(源码简化)
public class ThreadLocal<T> {
public void set(T value) {
// 1.获取当前线程
Thread t = Thread.currentThread();
// 2.从线程中拿到threadLoals
ThreadLocalMap map = getMap(t);
// 3.如果有map,往里存;如果没有map,就创建
if (map != null) {
// this 指的是当前 ThreadLocal 实例!
map.set(this, value);
} else {
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
② 看 ThreadLocal.get()
源码:
// ThreadLocal.java(源码简化)
public class ThreadLocal<T> {
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取当前线程的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果没有值,调用initialValue()
return setInitialValue();
}
}
2. 为什么ThreadLocalMap的key是弱引用
什么是弱引用
1、什么是弱引用?
类型 | 是否被 GC | 举例 |
---|---|---|
强引用(StrongReference) | 只要有引用,就不能被 GC | 正常的 new 出来的对象 |
弱引用(WeakReference) | 如果没有强引用,GC 就会清理 | new WeakReference<>(obj) |
示例:
public class LeakDemo {
public static void main(String[] args) {
// 1.创建ThreadLocal并设置值,但不保留引用
new ThreadLocal<>().set(new byte[1024*1024*10]); // 10MB
// 2.主线程结束后,我们期待GC回收内存
System.gc();
}
}
这个过程内存中发生了什么?---->
① 创建 ThreadLocal,并执行 set()
new ThreadLocal<>().set(...)
这一步发生了两件事:
- 创建了一个
ThreadLocal
实例(假设地址为 0x1001),但你没有变量保存这个实例 - 调用了
.set()
,当前线程的Thread.threadLocals
中,添加了这样一个 Entry:
Entry:
key = new WeakReference<ThreadLocal@0x1001>
value = new byte[10MB]
② 你没有保存 ThreadLocal 的引用
也就是说,只有 Thread 的 ThreadLocalMap 还保存了它的引用(key 是弱引用),其他地方都没有了。
此时 Java 的垃圾回收行为是:
- 发现 ThreadLocal@0x1001 没有强引用了
- 弱引用(WeakReference)不会阻止回收
- 所以下一次 GC 时,key 被清除了(key == null)
但是:
③ value 是强引用,仍然在内存中
虽然 key 被 GC 清掉了,但 value(10MB 的 byte[])是正常的强引用:
Entry:
key = null ❌ 被 GC 回收了
value = 10MB ✅ 还在!
这时候你已经再也拿不到这个 10MB 的 value,但 JVM 还留着它,不会回收!----> 可能导致的问题:
这就叫“内存泄漏”:你无法访问这块内存,但它还在 JVM 中,无法释放 —— 泄漏了。如果是线程池线程(长时间不销毁),这种垃圾会越来越多,最终导致内存溢出(OutOfMemoryError)。
为什么要用弱引用
2、那为什么要用弱引用?
你可能会问:那为啥不让 key 是强引用,就不会 key == null 了?----> 错误!如果是强引用,GC 永远不会清理这个 ThreadLocal 对象,即使你已经不想用了。这就变成:
你用完了 ThreadLocal,但 JVM 还一直“死抱着”它 ----> 也泄漏。
所以用弱引用是对的,只不过你用完后得自己把 value 给删掉!
也就是:ThreadLocalMap 的 key 是弱引用是为了防止强引用无法 GC,但 value 是强引用,如果你不手动 remove(),value 就永远留在内存中,造成泄漏。
// 避免泄漏 try { ThreadLocalHolder.set("value"); // 业务逻辑 } finally { ThreadLocalHolder.remove(); // 清理掉 Entry }
这样就会把 ThreadLocalMap 中的 Entry 移除,不会残留
value
问题:如果我手里有 ThreadLocal
的变量强引用(如下代码),那 ThreadLocalMap
中的 key 是不是强引用?----> 不是
ThreadLocal<String> local = new ThreadLocal<>();
local.set("abc");
ThreadLocalMap
中的 key 永远是弱引用。哪怕你手里保存了ThreadLocal
实例(比如变量local
)- 只是这个变量保护了 ThreadLocal 对象不被 GC。但是在
ThreadLocalMap
中,它仍然是通过WeakReference
被引用!
那手里有变量的强引用,有什么用?
- ThreadLocal 不会被 GC(被你引用着)
- 所以
ThreadLocalMap
的 key 仍然是弱引用,但不会被 GC 自动清除 - 一旦你手里的变量被设置为
null
,GC 就可以清除ThreadLocalMap
的 key
至此,文章结束了,记得小赞赞走起,主页还有更多精彩内容!!!