同一个线程里的几个方法需要共享一个值,但又不想让它与其他线程混在一起,可以用 ThreadLocal。它解决的是线程隔离,不是多个线程同时修改一个共享变量的问题。
每个线程自己的值
public class ThreadLocalDemo {
private static final ThreadLocal<Integer> LOCAL =
ThreadLocal.withInitial(() -> 0);
private static void runTask() {
try {
LOCAL.set(LOCAL.get() + 1);
System.out.println(Thread.currentThread().getName() + ":" + LOCAL.get());
} finally {
LOCAL.remove();
}
}
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(ThreadLocalDemo::runTask, "a");
Thread b = new Thread(ThreadLocalDemo::runTask, "b");
a.start();
b.start();
a.join();
b.join();
}
}两个线程各自输出 1,输出顺序不固定。LOCAL 虽然是同一个静态对象,但 a 和 b 取到的是各自保存的值。
withInitial() 指定第一次 get 时使用的初始化逻辑。remove 后再次 get,会重新走初始化过程。
值到底放在哪里
JDK 8 的 Thread 对象里有一个 threadLocals 字段,指向 ThreadLocalMap。可以把关系看成:
Thread
└─ ThreadLocalMap
├─ ThreadLocal A -> value A
└─ ThreadLocal B -> value B调用 get/set 时,先取得当前 Thread,再访问它的 Map。并不是 ThreadLocal 内部保存了一个以所有线程为键的大 Map。
因此任务换了一个线程,就不会自动拿到原线程设置的值。InheritableThreadLocal 能在创建子线程时继承初始值,但线程池里的工作线程会重复使用,不能据此实现每次任务提交时自动传递最新上下文。
隔离的是映射,不一定是对象
如果多个线程都执行 LOCAL.set(sharedObject),放进去的仍然是同一个对象引用。通过这些引用修改对象,依然存在共享访问。
要隔离可变数据,应让每个线程初始化自己的实例。比如旧的 SimpleDateFormat 可以按线程单独创建;如果能使用 Java 8 的 DateTimeFormatter,也可以直接利用它的不可变、线程安全特性。
什么时候清理
请求结束或任务结束后,最好在 finally 中 remove。尤其是线程池中的线程不会随着单个任务结束而消失,留下的值可能被后一个任务读取到。
ThreadLocal 能减少参数传递,却也让依赖变得不那么明显。只在一个方法里使用的数据,直接放局部变量更清楚;跨异步边界的上下文,则需要明确传递,而不是默认它会跟着任务走。