ThreadLocal 是 Java 中用于实现线程本地存储的类。如果ThreadLocal 变量是静态的(通常这样定义),则该 ThreadLocal 实例在类加载时只会被创建一次。ThreadLocal会存储每个线程的独立变量副本,这样可以避免多线程环境下的并发问题。
比较常用的场景:存储请求的上下文信息(比如用户信息),之后在代码调用链路中使用到时,通过上下文直接获取,减少数据之间的传递依赖性。
1. 基本概念
ThreadLocal 提供了线程局部变量。每个访问该变量的线程都拥有该变量的独立副本,互不干扰。它是一种非常简单且强大的机制,用于在多线程环境中保持数据的一致性和隔离性。
2. 使用方法
2.1 创建 ThreadLocal 变量
你可以使用 ThreadLocal 类来创建静态线程局部变量:
public class ThreadLocalExample {
//`ThreadLocal` 变量是静态的(通常这样定义),则该 `ThreadLocal` 实例在类加载时只会被创建一次
private static final ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(), "Thread-1");
Thread t2 = new Thread(new MyRunnable(), "Thread-2");
t1.start();
t2.start();
}
public static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
int currentValue = threadLocalValue.get();
threadLocalValue.set(currentValue + 1);
System.out.println(Thread.currentThread().getName() + " - " + threadLocalValue.get());
}
// 清理,以防内存泄漏
threadLocalValue.remove();
}
}
}
在这个例子中,每个线程都有自己独立的 threadLocalValue 副本,所以即使多个线程修改这个值,也不会相互影响。
2.2 方法介绍
get(): 返回当前线程中的此线程局部变量的值。set(T value): 将此线程局部变量的当前线程副本中的值设置为指定值。remove(): 移除当前线程中此线程局部变量的值,可以避免潜在的内存泄漏问题;
3. 实现原理
ThreadLocal 的实现依赖于内部维护的一个 ThreadLocalMap,map存储所有访问线程的局部变量副本。
ThreadLocal 的核心是通过 ThreadLocalMap 来保存和获取每个线程的局部变量值。
public class ThreadLocal<T> {
static class ThreadLocalMap {
// Entry 是继承自 WeakReference 的静态内部类
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
// 实现省略...
}
protected T initialValue() {
return null;
}
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;
}
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
// 获取当前线程的 ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
}
4. 注意事项
-
内存泄漏:
- 因为
ThreadLocalMap的键是弱引用,即使ThreadLocal对象被 GC 回收,其对应的值仍然可能存在于内存中,导致内存泄漏。 - 为了避免内存泄漏,在使用完
ThreadLocal后,应调用remove()方法来显式移除值。
- 因为
-
适用场景:
ThreadLocal适用于每个线程需要自己独立实例的场景,例如用户会话信息、数据库连接等。- 不适用于共享资源的情况,因为它的设计初衷就是为了隔离线程间的数据。
-
性能考虑:
- 虽然
ThreadLocal提供了一种方便的方式来处理线程本地变量,但频繁创建和销毁局部变量可能会带来一定的性能开销。因此,在性能敏感的应用中,需要权衡其使用。
- 虽然
5. 思考题
5.1 为什么使用弱引用?
ThreadLocal 的键是弱引用,这样设计的原因是为了允许垃圾回收器在没有外部引用时可以回收 ThreadLocal 实例,从而避免内存泄漏。但这同时也意味着需要开发者显式地清理不再使用的 ThreadLocal 值。
5.2 如果存在线程池调用ThreadLocal,会存在什么问题
- 线程复用:线程池中的线程通常会被重复使用,而不会在每次任务完成后销毁。这意味着同一个线程可能会处理多个不同的任务。
- 如果某个线程在执行完一个任务后,其
ThreadLocal变量没有清理,那么该变量及其值会一直存在于该线程中,当这个线程被线程池复用去执行另一个任务时,遗留的数据仍然存在,导致读取到之前的数据(比如用户B请求接口,读取到用户A的数据)。