目录
为什么Thread不能作为ThreadLocalMap的Key来使用
不用 ThreadLocal:靠参数 / 返回值传递,耦合度高
什么是ThreadLocal
ThreadLocal被称为线程局部变量,用于在线程中保存数据。由于在ThreadLocal中保存的数据仅属于当前线程,所以该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。
ThreadLocal用于在同一个线程间,在不同的类和方法之间共享数据的场景,也可以用于在不同线程间隔离数据的场景。
在 Java 中,每个 Thread
实例内部都维护着一个 ThreadLocalMap
对象(作为线程的私有属性),用于存储该线程的本地变量副本。ThreadLocal利用Thread中的ThreadLocalMap来进行数据存储
ThreadLocalMap内部结构
ThreadLocalMap内部数据结构是一个Entry类型的数组。每个Entry对象的Key为ThreadLocal对象,value为存储的数据
ThreadLocal的常用方法
存储数据void set(T value)
为当前线程单独存储一份变量副本,实现 “线程隔离”—— 不同线程通过同一个 ThreadLocal
对象存取值时,彼此互不干扰。
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果map已存在,就把当前ThreadLocal对象作为key,要存储的value作为值,存入map
if (map != null) {
map.set(this, value);
} else {
//如果 map 不存在(线程首次使用 ThreadLocal),则调用 createMap(t, value) 初始化 map 并存储数据。
createMap(t, value);
}
}
获取数据T get()
get()
方法用于获取当前线程中与该 ThreadLocal
对象关联的变量值,如果不存在则返回初始值 。
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 尝试从 ThreadLocalMap 中获取 Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果 ThreadLocalMap 为 null 或者没有找到对应的 Entry,返回初始值
return setInitialValue();
}
删除数据void remove()
ThreadLocal
的 remove()
方法的作用是删除当前线程中与该 ThreadLocal
对象关联的变量值,及时调用 remove()
方法可以避免内存泄漏,因为 ThreadLocalMap
中 Entry
的键是对 ThreadLocal
对象的弱引用,如果不手动删除,在 ThreadLocal
对象被回收后,Entry
中的值依然会强引用实际对象,导致对象无法被回收,从而造成内存泄漏。
public void remove() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从 ThreadLocalMap 中移除当前 ThreadLocal 对应的键值对
map.remove(this);
}
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 断开对 ThreadLocal 对象的弱引用
e.clear();
// 清理过期的 Entry
expungeStaleEntry(i);
return;
}
}
}
为什么用ThreadLocal做key
实现线程隔离
ThreadLocal
的核心目的是为每个线程提供独立的变量副本, 实现线程之间变量的隔离。使用ThreadLocal
作为ThreadLocalMap
的 key,能清晰地标识每个线程中独立的变量。- 例如,在一个多线程的 Web 应用里,每个请求线程都可能需要存储各自的用户会话信息 。不同的
ThreadLocal
实例就像不同的 “标签”,当一个线程通过某个ThreadLocal
实例调用set
方法时,实际上是在以这个ThreadLocal
实例为 key ,将对应的值存储到当前线程的ThreadLocalMap
中。而其他线程通过相同或不同的ThreadLocal
实例进行操作,访问的是各自线程ThreadLocalMap
里独立的数据, 从而保证了线程之间的数据不会相互干扰。
唯一标识变量
- 每个
ThreadLocal
实例都是唯一的。在程序中,可能会有多个不同用途的线程本地变量,使用ThreadLocal
实例作为 key, 可以准确地区分不同的线程本地变量。 - 比如,在一个复杂的多线程业务系统中,可能同时需要记录当前线程的请求 ID、用户权限等级等不同类型的线程本地信息。可以分别创建不同的
ThreadLocal
实例,如requestIdThreadLocal
、userPermissionThreadLocal
, 每个实例作为独立的 key ,将对应的数据存储到线程的ThreadLocalMap
中,方便后续根据不同的业务需求,准确获取对应的线程本地变量值。
配合弱引用设计
ThreadLocalMap
中Entry
的 key 是对ThreadLocal
的弱引用。这样设计的好处是,当ThreadLocal
实例在外部没有强引用指向时,在垃圾回收过程中可以被回收。- 假设没有使用弱引用,并且
ThreadLocal
实例一直被ThreadLocalMap
引用着,即使程序不再需要这个ThreadLocal
实例,它也无法被回收,就会造成内存泄漏。而使用弱引用,当ThreadLocal
实例失去外部强引用后,在下次垃圾回收时,ThreadLocalMap
中对应的 key 就会变为null
,虽然此时value
依然存在强引用,但在后续set
、get
、remove
等操作时,ThreadLocalMap
会对这种过期的Entry
进行清理, 降低了内存泄漏的风险。
简化使用逻辑
- 从代码使用的角度来看,使用
ThreadLocal
作为 key ,让ThreadLocal
类自身的操作逻辑更加简洁直观。开发者只需要通过调用ThreadLocal
实例的set
、get
、remove
等方法,就可以方便地操作线程本地变量, 无需额外管理复杂的 key 生成和存储逻辑。例如,ThreadLocal
的set
方法内部会自动将当前ThreadLocal
实例作为 key 进行存储操作,开发者不需要关心具体的 key 生成和ThreadLocalMap
的底层细节,提升了代码的易用性和可读性。
为什么Thread不能作为ThreadLocalMap的Key来使用
Thread当Key
-
当线程中只有一个
ThreadLocal
对象时:
此时线程只需要存储 “1 个变量”,用Thread
作为 key 确实能实现 “线程→变量” 的对应关系(每个线程一个值)。
但这是极端场景,实际开发中几乎不会出现 —— 一个线程往往需要存储多种上下文信息(如用户 ID、请求 ID、事务状态等)。 -
当线程中有多个
ThreadLocal
对象时:
假设用Thread
作为 key,那么所有变量都会被映射到同一个 key 上(当前线程实例),后存入的变量会覆盖先存入的变量,导致 “变量混乱”。
// 错误示例:用Thread作为key
Thread t = Thread.currentThread();
map.put(t, 用户信息); // 存储用户信息
map.put(t, 请求ID); // 覆盖用户信息(key相同)
此时无法同时保留 “用户信息” 和 “请求 ID”,失去了多变量存储的能力。
ThreadLocal当key
所以,不能使用Thread做key,而应该改用ThreadLocal对象做key,这样才能通过具体ThreadLocal对象的get()方法,获取到当前线程的ThreadLocalMap,然后进一步获取到对应的Entry(Entry当中的key可以直接一一对应ThreadLocal的实例)
ThreadLocalMap如何查找数据
当我们使用ThreadLocal获取当前线程中保存的Value数据时,是以ThreadLocal对象作为Key,通过访问Thread当前线程对象的内部ThreadLocalMap集合来获取到ValueThreadLocalMap集合的底层数据结构使用Entry[]数组保存键值对数据。所以,当通过ThreadLocal的get()、set()、remove()等方法,
int i = key.threadLocalHashCode & (table.length - 1);
通过key的 “hashCode值” 跟 “数组的长度减1” 做 “&按位与” 运算。其中key就是ThreadLocal对象。这种计算方式,相当于 “hashCode值” 跟 “数组的长度” 进行 “%取余” 运算,但是 “&按位与” 运算的效率更高
父子线程如何共享
在实际工作中,父线程中往ThreadLocal设置了值,在子线程中也能够获取到
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocal<String> threadLocal=new ThreadLocal<>();
threadLocal.set("肥嘟嘟左卫门");
System.out.println("主线程main:"+threadLocal.get());
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程:"+threadLocal.get());
}
});
thread.start();;
}
}
运行结果:
主线程main:肥嘟嘟左卫门
子线程:null
在这种情况下使用ThreadLocal是行不通的。main方法在主线程中执行,相当于父线程。在main方法中开启了另外一个线程,相当于子线程。两个线程,各自拥有不同的ThreadLocalMap,应该使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类
public class ThreadLocalTest2 {
public static void main(String[] args) {
InheritableThreadLocal threadLocal=new InheritableThreadLocal();
threadLocal.set("肥嘟嘟左卫门");
System.out.println("主线程main:"+threadLocal.get());
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程:"+threadLocal.get());
}
});
thread.start();
}
}
运行结果:
主线程main:肥嘟嘟左卫门
子线程:肥嘟嘟左卫门
ThreadLocal如何避免内存泄漏
threadLocal为什么会产生内存泄漏
ThreadLocalMap的内部数据结构Entry继承自WeakReference<ThreadLocal<?>>
这意味着Entry对ThreadLocal的引用是弱引用。根据弱引用的特性,当ThreadLocal对象没有其他强引用指向它时,在垃圾回收发生时,这个ThreadLocal对象就会被回收,此时Entry中key(ThreadLocal对象)就会变为null。但是,Entry中的value仍然被Entry对象强引用着,并且由于线程没有结束,ThreadLocalMap也不会被回收,这样value就一直无法被回收,从而造成了内存泄漏。
如何避免内存泄漏
使用完毕后,在finally调用ThreadLocal对象的remove()方法
以下是一个实际业务场景的示例(模拟用户登录后,在请求线程中存储用户信息,请求结束后清理):
import java.util.UUID;
public class ThreadLocalDemo {
// 定义ThreadLocal,存储当前线程的用户ID(假设业务中需要在多个方法间共享用户信息)
private static final ThreadLocal<String> CURRENT_USER_ID = new ThreadLocal<>();
public static void main(String[] args) {
// 模拟一个用户请求处理过程
handleUserRequest("user_123");
}
/**
* 处理用户请求的方法
*/
public static void handleUserRequest(String userId) {
try {
// 1. 存储用户信息到ThreadLocal
CURRENT_USER_ID.set(userId);
System.out.println("请求开始,存储用户ID:" + userId);
// 2. 模拟业务处理(可能调用多个工具类/服务,需要获取当前用户ID)
doBusiness1();
doBusiness2();
} finally {
// 3. 无论业务处理成功与否,最终都要清理ThreadLocal
CURRENT_USER_ID.remove();
System.out.println("请求结束,清理ThreadLocal,当前值:" + CURRENT_USER_ID.get()); // 清理后为null
}
}
// 业务方法1:需要获取当前用户ID
private static void doBusiness1() {
String userId = CURRENT_USER_ID.get();
System.out.println("业务1处理中,当前用户ID:" + userId);
}
// 业务方法2:需要获取当前用户ID
private static void doBusiness2() {
String userId = CURRENT_USER_ID.get();
System.out.println("业务2处理中,当前用户ID:" + userId);
}
}
remove()方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄漏问题
ThreadLcoal应用场景
线程数据隔离
ThreadLocal的主要价值在于线程隔离,ThreadLocal中的数据只属于当前线程,该线程对别的线程是不可见的,起到了隔离作用。在多线程环境下,可以防止当前线程的数据被其他线程修改。另外,由于各个线程之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提升了并发性的性能
比如 APP 里,10 个线程同时查 10 个用户的订单记录,每个线程要暂存 “当前查的用户 ID”,还得保证不串线(线程 A 的用户 ID 不能被线程 B 改了)。
用 ThreadLocal 就特简单:
// 1. 造个ThreadLocal,存当前线程的用户ID
static ThreadLocal<String> currUserId = new ThreadLocal<>();
public static void main(String[] args) {
// 2. 开3个线程,分别查用户1、2、3的订单
new Thread(() -> {
currUserId.set("用户1"); // 线程1存自己的用户ID
getOrder(); // 查订单时直接取,不用传参数
currUserId.remove(); // 用完清掉
}, "线程1").start();
new Thread(() -> {
currUserId.set("用户2"); // 线程2存自己的
getOrder();
currUserId.remove();
}, "线程2").start();
}
// 查订单的方法
static void getOrder() {
// 直接取当前线程的用户ID,不用传参,还不会和其他线程串
String userId = currUserId.get();
System.out.println(Thread.currentThread().getName() + "查:" + userId + "的订单");
}
线程 1 查用户 1,线程 2 查用户 2,互不干扰。
要是不用 ThreadLocal,要么得给getOrder()
传参数(多层调用会很麻烦),要么用锁(多线程排队,慢)——ThreadLocal 既不用传参,又不用等锁,这就是它的好~
跨函数传递
数据通常用于同一个线程内,跨类、跨方法传递数据时,如果不用ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度
举个栗子🌰🌰🌰:
假设一个简单的电商下单场景:从 “接收请求” 到 “创建订单”,要经过 3 个类(OrderController
、OrderService
、OrderDAO
),每个环节都需要 “当前用户 ID”。
不用 ThreadLocal:靠参数 / 返回值传递,耦合度高
// 1. 控制器:接收请求,拿到用户ID,传给Service
class OrderController {
private OrderService orderService = new OrderService();
// 下单接口:必须把userId作为参数传给service
public void createOrder(String userId, String goodsId) {
System.out.println("控制器拿到用户ID:" + userId);
orderService.doCreateOrder(userId, goodsId); // 传userId
}
}
// 2. 服务层:拿到userId,再传给DAO
class OrderService {
private OrderDAO orderDAO = new OrderDAO();
// 必须接收userId参数,再往下传
public void doCreateOrder(String userId, String goodsId) {
System.out.println("服务层拿到用户ID:" + userId);
// 还可能调用其他工具类(比如日志、权限校验),也要传userId
LogUtils.log(userId, "开始创建订单");
orderDAO.saveOrder(userId, goodsId); // 传userId
}
}
// 3. DAO层:最终用userId存订单
class OrderDAO {
// 必须接收userId参数
public void saveOrder(String userId, String goodsId) {
System.out.println("DAO层用用户ID存订单:" + userId + "-" + goodsId);
}
}
// 测试
public class Test {
public static void main(String[] args) {
OrderController controller = new OrderController();
controller.createOrder("用户123", "商品456");
}
}
问题:耦合度高
- 每个方法都要加
userId
参数,哪怕方法本身不 “直接需要” 这个参数(比如OrderService
的核心逻辑是处理订单,却必须传userId
给 DAO); - 要是后续需要加新参数(比如 “用户等级”),
controller→service→DAO
的所有方法都要改参数,牵一发动全身。
用 ThreadLocal:不用传参,解耦
// 1. 用ThreadLocal存当前线程的用户ID(全局工具类)
class UserContext {
public static ThreadLocal<String> currUserId = new ThreadLocal<>();
}
// 2. 控制器:存用户ID,不用传参给Service
class OrderController {
private OrderService orderService = new OrderService();
public void createOrder(String userId, String goodsId) {
UserContext.currUserId.set(userId); // 存到ThreadLocal
System.out.println("控制器存用户ID:" + userId);
orderService.doCreateOrder(goodsId); // 不用传userId了!
UserContext.currUserId.remove(); // 用完清理
}
}
// 3. 服务层:直接取,不用接收参数,也不用传DAO
class OrderService {
private OrderDAO orderDAO = new OrderDAO();
// 方法参数里没有userId了!
public void doCreateOrder(String goodsId) {
String userId = UserContext.currUserId.get(); // 直接取
System.out.println("服务层拿到用户ID:" + userId);
LogUtils.log(userId, "开始创建订单"); // 工具类也直接取
orderDAO.saveOrder(goodsId); // 不用传userId!
}
}
// 4. DAO层:直接取,不用接收参数
class OrderDAO {
public void saveOrder(String goodsId) {
String userId = UserContext.currUserId.get(); // 直接取
System.out.println("DAO层用用户ID存订单:" + userId + "-" + goodsId);
}
}
优势:解耦
- 方法不用再带 “传递用” 的参数(
userId
),每个类 / 方法只关注自己的核心逻辑(控制器管接收请求、服务层管业务、DAO 管存数据); - 后续加新参数(比如 “用户等级”),只需在
UserContext
加新的 ThreadLocal,不用改所有方法的参数列表,改动极小。
简单说:不用 ThreadLocal,参数像 “接力棒” 一样在类 / 方法间传;用了 ThreadLocal,参数像 “自己的口袋”,每个线程自己存、自己取,不用麻烦别人传,类和方法之间就松绑了~
以上从 ThreadLocal 的核心定义、底层结构,到常用方法、关键设计逻辑(如为何用 ThreadLocal 做 key),再到实际应用场景(线程隔离、跨方法传参)与内存泄漏解决方案,完整梳理了 ThreadLocal 的核心知识点。它看似简单,却是多线程开发中 “解耦” 与 “提效” 的关键工具 —— 用好 ThreadLocal 能避免冗余参数传递和锁竞争,用错则可能引发内存泄漏。希望这份总结能帮你真正理解并合理运用 ThreadLocal,在实际开发中少踩坑、高效解决线程数据管理问题~
双节将至,秋意正浓!小编提前送上祝福:愿大家国庆玩得尽兴,中秋吃得香甜,日子越过越圆满,平安喜乐常相伴~