多线程是 Java 实现多任务的基础,Thread 对象代表一个线程,我们可以在代码中调用 Thread.currentThread() 获取当前线程。例如,打印日志时,可以同时打印出当前线程的名字:

  1. public class Main {
  2. public static void main(String[] args) throws Exception {
  3. log("start main...");
  4. new Thread(() -> {
  5. log("run task...");
  6. }).start();
  7. new Thread(() -> {
  8. log("print...");
  9. }).start();
  10. log("end main.");
  11. }
  12. static void log(String s) {
  13. System.out.println(Thread.currentThread().getName() + ": " + s);
  14. }
  15. }

output:

  1. main: start main...
  2. main: end main.
  3. Thread-0: run task...
  4. Thread-1: print...

对于多任务,Java 标准库提供的线程池可以方便地执行这些任务,同时复用线程。Web 应用程序就是典型的多任务应用,每个用户请求页面时,我们都会创建一个任务,类似:

  1. public void process(User user) {
  2. checkPermission();
  3. doWork();
  4. saveStatus();
  5. sendResponse();
  6. }

然后,通过线程池去执行这些任务。
观察 process() 方法,它内部需要调用若干其他方法,同时,我们遇到一个问题:如何在一个线程内传递状态
process() 方法需要传递的状态就是 User 实例。可能你会想到,简单地传入 User 就可以了:

  1. public void process(User user) {
  2. checkPermission(user);
  3. doWork(user);
  4. saveStatus(user);
  5. sendResponse(user);
  6. }

但是往往一个方法又会调用其他很多方法,这样会导致 User 传递到所有地方:

  1. void doWork(User user) {
  2. queryStatus(user);
  3. checkStatus();
  4. setNewStatus(user);
  5. log();
  6. }

这种在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。
给每个方法增加一个 context 参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User 对象就传不进去了。
Java 标准库提供了一个特殊的 ThreadLocal,它可以在一个线程中传递同一个对象。
ThreadLocal 实例通常总是以静态字段初始化如下:

  1. static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();

使用方法:

  1. void processUser(user) {
  2. try {
  3. threadLocalUser.set(user);
  4. step1();
  5. step2();
  6. } finally {
  7. threadLocalUser.remove();
  8. }
  9. }

通过设置一个 User 实例关联到 ThreadLocal 中,在移除之前,所有方法都可以随时获取到该 User 实例:

  1. void step1() {
  2. User u = threadLocalUser.get();
  3. log();
  4. printUser();
  5. }
  6. void log() {
  7. User u = threadLocalUser.get();
  8. println(u.name);
  9. }
  10. void step2() {
  11. User u = threadLocalUser.get();
  12. checkUser(u.id);
  13. }

注意到普通的方法调用一定是同一个线程执行的,所以,step1()step2() 以及 log() 方法内, threadLocalUser.get() 获取的 User 对象是同一个实例。

实际上,可以把 ThreadLocal 看成一个全局 Map:每个线程获取 ThreadLocal 变量时,总是使用 Thread 自身作为 key: Object threadLocalValue = threadLocalMap.get(Thread.currentThread());

因此,ThreadLocal 相当于给每个线程都开辟了一个独立的存储空间,各个线程的 ThreadLocal 关联的实例互不干扰。

最后,特别注意 ThreadLocal 一定要在 finally 中清除:

  1. try {
  2. threadLocalUser.set(user);
  3. ...
  4. } finally {
  5. threadLocalUser.remove(); // clear ThreadLocal
  6. }

这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果 ThreadLocal 没有被清除,该线程执行其他代码时,会把上一次的状态带进去。
为了保证能释放 ThreadLocal 关联的实例,我们可以通过 AutoCloseable 接口配合 try (resource) {...} 结构,让编译器自动为我们关闭。
例如,一个保存了当前用户名的 ThreadLocal 可以封装为一个 UserContext 对象:

  1. public class UserContext implements AutoCloseable {
  2. static final ThreadLocal<String> ctx = new ThreadLocal<>();
  3. public UserContext(String user) {
  4. ctx.set(user);
  5. }
  6. public static String currentUser() {
  7. return ctx.get();
  8. }
  9. @Override
  10. public void close() {
  11. ctx.remove();
  12. }
  13. }

使用的时候,我们借助 try (resource) {...} 结构,可以这么写:

  1. try (var ctx = new UserContext("Bob")) {
  2. // 可任意调用 UserContext.currentUser():
  3. String currentUser = UserContext.currentUser();
  4. } // 在此自动调用 UserContext.close() 方法释放 ThreadLocal 关联对象

这样就在 UserContext 中完全封装了 ThreadLocal,外部代码在 try (resource) {...} 内部可以随时调用 UserContext.currentUser() 获取当前线程绑定的用户名。

小结

ThreadLocal 表示线程的「局部变量」,它确保每个线程的 ThreadLocal 变量都是各自独立的;
ThreadLocal 适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递);
使用 ThreadLocal 要用 try ... finally 结构,并在 finally 中清除。