引言
在多线程编程中,如何安全地共享数据是一个重要的课题。Java 提供了 ThreadLocal 类,以便在每个线程中维护线程局部变量,允许每个线程拥有自己的独立变量副本。本文将探讨 ThreadLocal 的工作原理、使用场景以及一些最佳实践。
1. 什么是 ThreadLocal?
ThreadLocal 是 Java 提供的一个类,它用于创建线程局部变量。每个线程在访问 ThreadLocal 变量时,都会获取到一个与其他线程隔离的副本。这意味着,一个线程对 ThreadLocal 变量的修改不会影响其他线程。
当你创建一个 ThreadLocal 变量并调用 get() 方法时,会为当前线程创建一个新的副本(如果它之前没有)。每个线程都有一个 ThreadLocalMap,其中存储了所有 ThreadLocal 变量的值。这个映射只在当前线程的上下文中可见,从而实现了数据的隔离。
Thread、ThreadLocalMap、ThreadLocal结构关系图如下:
- 每个Thread都有一个ThreadLocalMap变量
- ThreadLocalMap内部定义了Entry(ThreadLocal<?> k, Object v)节点类,在 Entry中,ThreadLocal 实例作为键(key),而通过 set(value) 方法存储的值作为value
- 这个Entry节点继承了WeakReference类泛型为ThreacLocal类
2. ThreadLocal主要方法解析
2.1 ThreadLocal 的 set 方法
set方法的源码如下:
/**
* 为当前 ThreadLocal 对象关联 value 值
* @param value 要存储在此线程的线程副本的值
*/
public void set(T value) {
// 返回当前ThreadLocal所在的线程
Thread t = Thread.currentThread();
// 返回当前线程持有的map
ThreadLocalMap map = getMap(t);
if (map != null) {
// 如果 ThreadLocalMap 不为空,则直接存储<ThreadLocal, T>键值对
map.set(this, value);
} else {
// 否则,需要为当前线程初始化 ThreadLocalMap,并存储键值对 <this, firstValue>
createMap(t, value);
}
}
set 方法主要流程为:
- 先获取到当前线程的引用
- 利用这个引用来获取到 ThreadLocalMap
- 如果 map 为空,则去创建一个 ThreadLocalMap
- 如果 map 不为空,就利用 ThreadLocalMap 的 set 方法将 value 添加到 map 中
2.2 ThreadLocal 的 get 方法
get方法的源码如下:
/**
* 返回当前 ThreadLocal 对象关联的值
*
* @return
*/
public T get() {
// 返回当前 ThreadLocal 所在的线程
Thread t = Thread.currentThread();
// 从线程中拿到 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从 map 中拿到 entry
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果不为空,读取当前 ThreadLocal 中保存的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
// 若 map 为空,则对当前线程的 ThreadLocal 进行初始化,初始化的值为null
return setInitialValue();
}
get 方法的主要流程为:
- 先获取到当前线程的引用
- 获取当前线程内部的 ThreadLocalMap
- 如果 map 存在,则获取当前 ThreadLocal 对应的 value 值
- 如果 map 不存在或者找不到 value 值,则调用 setInitialValue() 进行初始化
2.3 ThreadLocal 的 remove 方法
remove方法的源码如下:
/**
* 清理当前 ThreadLocal 对象关联的键值对
*/
public void remove() {
// 返回当前线程持有的 map
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
// 从 map 中清理当前 ThreadLocal 对象关联的键值对
m.remove(this);
}
}
remove方法的主要流程为:
- 先获取到当前线程的 ThreadLocalMap,并且调用了它的 remove 方法
- 然后从 map 中清理当前 ThreadLocal 对象关联的键值对,这样 value 就可以被 GC 回收了
3. ThreadLocal内存泄漏问题
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 其他方法...
}
-
由ThreadLocalMap 的源码我们知道,ThreadLocal 实例存储在 ThreadLocalMap 的Entry中,Entry的key为ThreadLocal对象的弱引用,但是映射值(即 Entry 中的value)是强引用。
-
当方法执行过程中,由于栈帧销毁或者主动释放等原因,释放了 ThreadLoca l对象的强引用,即表示该 ThreadLocal 对象可以被回收了。又因为Entry中key为ThreadLocal对象的弱引用,所以当JVM执行GC操作时是能够回收该ThreadLocal对象的。
-
而Entry中value对应的是变量实体对象的强引用,因此释放一个ThreadLocal对象,是无法释放ThreadLocalMap中对应的value对象的,也就造成了内存泄漏。除非释放当前线程对象。但是日常开发中会经常使用线程池等线程池化技术,释放线程对象的条件往往无法达到。
因此,在使用完 ThreadLocal 变量后,需要我们手动 remove 掉,防止 ThreadLocalMap 中 Entry 一直保持对 value 的强引用,导致 value 不能被回收。
例子:
public class ThreadLocalExample {
// 创建一个 ThreadLocal 变量
private static ThreadLocal<String> threadLocalValue = ThreadLocal.withInitial(() -> "Initial Value");
public static void main(String[] args) {
// 创建多个线程来演示 ThreadLocal 的使用
Runnable task = () -> {
// 设置 ThreadLocal 的值
threadLocalValue.set(Thread.currentThread().getName() + " - ThreadLocal Value");
// 获取并打印 ThreadLocal 的值
System.out.println(threadLocalValue.get());
// 重要:使用完后清理 ThreadLocal 的值
threadLocalValue.remove();
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
Thread thread3 = new Thread(task);
thread1.start();
thread2.start();
thread3.start();
// 等待线程执行完成
try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
执行结果:
Thread-0 - ThreadLocal Value
Thread-1 - ThreadLocal Value
Thread-2 - ThreadLocal Value
**/
4. ThreadLocal应用场景
ThreadLocal 的特性也导致了应用场景比较广泛,主要的应用场景如下:
- 线程间数据隔离,各线程的 ThreadLocal 互不影响
- 方便同一个线程使用某一对象,避免不必要的参数传递
- 全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal
- Spring 事务管理器采用了 ThreadLocal
- Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal