Jakob Jenkov 2020-09-28
Java ThreadLocal 类允许你创建只能被同一线程读取和写入的变量。因此,即使两个线程执行相同的代码,并且该代码引用了同一个 ThreadLocal 变量,这两个线程也无法看到彼此的 ThreadLocal 变量。因此,Java ThreadLocal 类提供了一种简单的方法,使原本非线程安全(thread unsafe)的代码变得线程安全(thread safe)。
创建 ThreadLocal
你可以像创建任何其他 Java 对象一样,通过 new 操作符来创建一个 ThreadLocal 实例。下面是一个创建 ThreadLocal 变量的示例:
private ThreadLocal threadLocal = new ThreadLocal();
每个线程只需执行一次此操作。多个线程现在可以在该 ThreadLocal 中设置和获取值,而每个线程只能看到自己设置的值。
设置 ThreadLocal 的值
创建 ThreadLocal 后,你可以使用其 set() 方法来存储值:
threadLocal.set("A thread local value");
获取 ThreadLocal 的值
你可以使用 get() 方法读取 ThreadLocal 中存储的值。以下是从 Java ThreadLocal 中获取值的示例:
String threadLocalValue = (String) threadLocal.get();
移除 ThreadLocal 的值
可以移除 ThreadLocal 变量中设置的值。通过调用 ThreadLocal 的 remove() 方法即可实现。以下是移除 Java ThreadLocal 中值的示例:
threadLocal.remove();
泛型 ThreadLocal
你可以创建带有泛型类型的 ThreadLocal。使用泛型后,只能将指定类型的对象设置为 ThreadLocal 的值。此外,从 get() 返回的值也不需要强制类型转换。下面是一个泛型 ThreadLocal 的示例:
private ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();
现在你只能在该 ThreadLocal 实例中存储字符串。此外,你也不需要对从 ThreadLocal 获取的值进行类型转换:
myThreadLocal.set("Hello ThreadLocal");
String threadLocalValue = myThreadLocal.get();
ThreadLocal 的初始值
可以为 Java ThreadLocal 设置一个初始值,该值将在首次调用 get()(在调用 set() 之前)时使用。有两种方式可以指定 ThreadLocal 的初始值:
- 创建一个
ThreadLocal的子类并重写initialValue()方法。 - 使用
Supplier接口实现创建ThreadLocal。
下面几节将分别展示这两种方法。
重写 initialValue() 方法
第一种方式是创建一个 ThreadLocal 的子类,并重写其 initialValue() 方法。最简单的方式是在创建 ThreadLocal 变量的位置直接创建一个匿名子类。以下是一个创建匿名子类并重写 initialValue() 方法的示例:
private ThreadLocal<String> myThreadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return String.valueOf(System.currentTimeMillis());
}
};
注意:不同线程仍然会看到不同的初始值。每个线程都会创建自己的初始值。只有当你从 initialValue() 方法返回完全相同的对象时,所有线程才会看到同一个对象。然而,使用 ThreadLocal 的初衷正是为了避免多个线程看到同一个实例。
提供 Supplier 实现
第二种方式是使用 ThreadLocal 的静态工厂方法 withInitial(Supplier),并传入一个 Supplier 接口的实现。这个 Supplier 实现负责提供 ThreadLocal 的初始值。以下是一个使用 withInitial() 静态工厂方法并传入简单 Supplier 实现的示例:
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(new Supplier<String>() {
@Override
public String get() {
return String.valueOf(System.currentTimeMillis());
}
});
由于 Supplier 是一个函数式接口,可以用 Java Lambda 表达式 来实现。下面是使用 Lambda 表达式作为 Supplier 实现传递给 withInitial() 的写法:
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> {
return String.valueOf(System.currentTimeMillis());
});
如你所见,这比之前的示例更简洁。甚至还能进一步简化为最紧凑的 Lambda 语法:
ThreadLocal<String> threadLocal3 = ThreadLocal.withInitial(
() -> String.valueOf(System.currentTimeMillis())
);
延迟设置 ThreadLocal 的值
在某些情况下,你无法使用上述标准方式设置初始值。例如,你可能需要一些在创建 ThreadLocal 变量时尚不可用的配置信息。在这种情况下,你可以延迟设置初始值。以下是一个在 Java ThreadLocal 上延迟设置初始值的示例:
public class MyDateFormatter {
private ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<>();
public String format(Date date) {
SimpleDateFormat simpleDateFormat = getThreadLocalSimpleDateFormat();
return simpleDateFormat.format(date);
}
private SimpleDateFormat getThreadLocalSimpleDateFormat() {
SimpleDateFormat simpleDateFormat = simpleDateFormatThreadLocal.get();
if (simpleDateFormat == null) {
simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
simpleDateFormatThreadLocal.set(simpleDateFormat);
}
return simpleDateFormat;
}
}
注意:format() 方法调用了 getThreadLocalSimpleDateFormat() 方法以获取一个 Java SimpleDateFormat 实例。如果 ThreadLocal 中尚未设置 SimpleDateFormat 实例,则会创建一个新的 SimpleDateFormat 并将其存入 ThreadLocal 变量中。
一旦某个线程在 ThreadLocal 变量中设置了它自己的 SimpleDateFormat,那么该线程后续将一直使用这个 SimpleDateFormat 对象。但仅限于该线程。每个线程都会创建自己的 SimpleDateFormat 实例,因为它们无法看到其他线程在 ThreadLocal 变量中设置的实例。
SimpleDateFormat 类不是线程安全的,因此多个线程不能同时使用它。为了解决这个问题,上面的 MyDateFormatter 类为每个线程创建了一个 SimpleDateFormat,这样每个调用 format() 方法的线程都会使用自己的 SimpleDateFormat 实例。
在线程池或 ExecutorService 中使用 ThreadLocal
如果你打算在提交给 Java 线程池 或 Java ExecutorService 的任务中使用 Java ThreadLocal,请记住:你无法保证哪个线程会执行你的任务。
然而,如果你只需要确保每个线程使用自己独有的某个对象实例,那就不是问题。在这种情况下,你完全可以在线程池或 ExecutorService 中正常使用 Java ThreadLocal。
完整的 ThreadLocal 示例
以下是一个完整的、可运行的 Java ThreadLocal 示例:
public class ThreadLocalExample {
public static void main(String[] args) {
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread thread1 = new Thread(sharedRunnableInstance);
Thread thread2 = new Thread(sharedRunnableInstance);
thread1.start();
thread2.start();
thread1.join(); // 等待线程1结束
thread2.join(); // 等待线程2结束
}
}
public class MyRunnable implements Runnable {
private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
@Override
public void run() {
threadLocal.set((int) (Math.random() * 100D));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(threadLocal.get());
}
}
这个例子创建了一个 MyRunnable 实例,并将其传递给两个不同的线程。两个线程都执行 run() 方法,因此在 ThreadLocal 实例中设置了不同的值。
如果访问 set() 调用是同步的,并且它不是一个 ThreadLocal 对象,那么第二个线程将会覆盖第一个线程设置的值。
但由于这是一个 ThreadLocal 对象,两个线程无法看到彼此的值。因此,它们设置和获取的是不同的值。
InheritableThreadLocal
InheritableThreadLocal 类是 ThreadLocal 的一个子类。与每个线程在 ThreadLocal 中拥有自己的值不同,InheritableThreadLocal 允许一个线程及其创建的所有子线程访问相同的值。
以下是一个完整的 Java InheritableThreadLocal 示例:
public class InheritableThreadLocalBasicExample {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
Thread thread1 = new Thread(() -> {
System.out.println("===== Thread 1 =====");
threadLocal.set("Thread 1 - ThreadLocal");
inheritableThreadLocal.set("Thread 1 - InheritableThreadLocal");
System.out.println(threadLocal.get());
System.out.println(inheritableThreadLocal.get());
Thread childThread = new Thread(() -> {
System.out.println("===== ChildThread =====");
System.out.println(threadLocal.get());
System.out.println(inheritableThreadLocal.get());
});
childThread.start();
});
thread1.start();
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("===== Thread2 =====");
System.out.println(threadLocal.get());
System.out.println(inheritableThreadLocal.get());
});
thread2.start();
}
}
此示例创建了一个普通的 Java ThreadLocal 和一个 Java InheritableThreadLocal。然后,示例创建了一个线程,在其中设置了 ThreadLocal 和 InheritableThreadLocal 的值,并创建了一个子线程来访问这两个变量的值。只有 InheritableThreadLocal 的值对子线程可见。
最后,示例又创建了第三个线程,它也尝试访问这两个变量,但它看不到第一个线程存储的任何值。
运行此示例的输出如下:
===== Thread 1 =====
Thread 1 - ThreadLocal
Thread 1 - InheritableThreadLocal
===== ChildThread =====
null
Thread 1 - InheritableThreadLocal
===== Thread2 =====
null
null