同一个线程里的几个方法需要共享一个值,但又不想让它与其他线程混在一起,可以用 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 能减少参数传递,却也让依赖变得不那么明显。只在一个方法里使用的数据,直接放局部变量更清楚;跨异步边界的上下文,则需要明确传递,而不是默认它会跟着任务走。

最后修改:2018 年 03 月 25 日
如果觉得我的文章对你有用,请随意赞赏