ThreadLocal,线程本地变量,可以理解为一个线程内部的全局变量。但是变量名是多线程可以共用的。
对于 ThreadLocal 的原理,可以简单地理解为内部用了一个 Map,key是线程对象,value是值
(注意,实际不是这样实现的)。当前的线程对象很好拿,就是Thread.currentThread()
。
常见方法:
方法名 | 方法属性 | 描述 |
---|---|---|
set | 实例方法 | 设置值 |
get | 实例方法 | 获取值 |
remove | 实例方法 | 清空值。清空后,若再获取值,会重新初始化值。 |
withInitial | 静态方法 | 指定如何初始化值,并生成 ThresholdLocal 对象。如果没有用该方法生成 ThresholdLocal 对象,那么 ThresholdLocal 对象的初始化值是 null。 |
基本使用
在一个线程中使用 ThreadLocal 。
public class ThreadLocalTest {
private static ThreadLocal<Integer> logId = new ThreadLocal<>();
// 输出线程名、logId在当前线程的值
public static void showLogId() {
System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
}
public static void main(String[] args) {
showLogId();
logId.set(10);
showLogId();
logId.set(20);
showLogId();
logId.remove();
showLogId();
}
}
运行后输出:
main : null
main : 10
main : 20
main : null
main
是主线程的名字。可以看到 ThreadLocal 变量的初始值是 null 。
我们再看下 withInitial 方法的使用:
public class ThreadLocalTest {
private static ThreadLocal<Integer> logId = ThreadLocal.withInitial(() -> {
return 1; // 这里只是一个示例,实际业务场景中可能是从数据源(如MySQL)中取数据
});;
public static void showLogId() {
System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
}
public static void main(String[] args) {
showLogId();
logId.set(20);
showLogId();
logId.remove();
showLogId();
}
}
运行结果:
main : 1
main : 20
main : 1
在多线程中使用
在多个线程中使用 ThreadLocal 。下面的 logId、showLogId 来自上面的示例。
showLogId();
logId.set(10);
showLogId();
Thread t1 = new Thread(()->{
showLogId();
logId.set(20);
showLogId();
});
Thread t2 = new Thread(()->{
showLogId();
logId.set(30);
showLogId();
});
t1.start(); // 运行线程 t1
t2.start(); // 运行线程 t2
t1.join(); // 等待线程 t1 执行完
t2.join(); // 等待线程 t2 执行完
showLogId(); // 再看下当前线程的 logId 值
运行结果:
main : null
main : 10
Thread-0 : null
Thread-0 : 20
Thread-1 : null
Thread-1 : 30
main : 10
可以看到,在主线程、Thread-0、Thread-1三个线程中,logId 的初始值都是 null,在一个线程中对 logId 设值,不应影响另外一个线程中的 logId 值。也就是 logId 在不同线程中互相隔离的。
在线程池中使用可能遇到问题
如果线程池中线程会被复用,这时 ThreadLocal 的值也会被复用。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadLocalTest {
private static ThreadLocal<Integer> logId = new ThreadLocal<>();
public static void showLogId() {
System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
}
public static void main(String[] args) throws InterruptedException {
showLogId();
logId.set(10);
showLogId();
// 只有一个线程的线程池,该线程会复用
ExecutorService executorService = Executors.newFixedThreadPool(1);
// 提交task
executorService.submit(() -> {
showLogId();
logId.set(20);
showLogId();
});
// 再提交一个task
executorService.submit(() -> {
showLogId();
logId.set(30);
showLogId();
});
// 关闭线程池
executorService.shutdown();
// 等待至任务都执行完,最多等3秒
executorService.awaitTermination(3, TimeUnit.SECONDS);
showLogId();
}
}
输出:
main : null
main : 10
pool-1-thread-1 : null
pool-1-thread-1 : 20
pool-1-thread-1 : 20
pool-1-thread-1 : 30
main : 10
可以看出来,我们在线程池中提交了两个任务,这两个任务都在一个线程中执行,也就是线程被复用了,所以 ThreadLocal 类型的 logId 也被复用了。
这个示例没有区分出两个task,我们优化下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class ThreadLocalTest {
private static ThreadLocal<Integer> logId = new ThreadLocal<>();
private static AtomicLong taskId = new AtomicLong(); // 生成任务编号
public static void showLogId() {
System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
}
public static void main(String[] args) throws InterruptedException {
showLogId();
logId.set(10);
showLogId();
// 只有一个线程的线程池,该线程会复用
ExecutorService executorService = Executors.newFixedThreadPool(1);
// 提交task
executorService.submit(() -> {
Thread.currentThread().setName("task-" + taskId.addAndGet(1));
showLogId();
logId.set(20);
showLogId();
});
// 再提交一个task
executorService.submit(() -> {
Thread.currentThread().setName("task-" + taskId.addAndGet(1));
showLogId();
logId.set(30);
showLogId();
});
// 关闭线程池
executorService.shutdown();
// 等待至任务都执行完,最多等3秒
executorService.awaitTermination(3, TimeUnit.SECONDS);
showLogId();
}
}
运行后输出:
main : null
main : 10
task-1 : null
task-1 : 20
task-2 : 20
task-2 : 30
main : 10
可以看到,线程被复用时,ThreadLocal 也被复用了,这可能不符合我们的预期,那怎么办?
我们可以对 Runnable 进行改造,见下面的示例。
如何避免 ThreadLocal 在线程池中被复用
如何避免线程池中线程被复用而导致的 ThreadLocal 被复用的问题呢 ?一个简单的思路是,我们对 Runnable 进行改造。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class ThreadLocalTest {
private static ThreadLocal<Integer> logId = new ThreadLocal<>();
private static AtomicLong taskId = new AtomicLong(); // 生成任务编号
// 加强版 Runnable
public static class EnhancedRunnable implements Runnable {
private Runnable delegate;
public EnhancedRunnable(Runnable task) {
delegate = task; // 在当前线程执行
}
public static EnhancedRunnable of(Runnable task) {
return new EnhancedRunnable(task);
}
@Override
public void run() { // 在另外一个线程执行
logId.remove(); // 清理 logId 的值
delegate.run();
}
}
public static void showLogId() {
System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
}
public static void main(String[] args) throws InterruptedException {
showLogId();
logId.set(10);
showLogId();
// 只有一个线程的线程池,该线程会复用
ExecutorService executorService = Executors.newFixedThreadPool(1);
// 提交 EnhancedRunnable 类型的任务
executorService.submit(EnhancedRunnable.of(() -> {
Thread.currentThread().setName("task-" + taskId.addAndGet(1));
showLogId();
logId.set(20);
showLogId();
}));
// 再提交一个 EnhancedRunnable 类型的任务
executorService.submit(EnhancedRunnable.of(() -> {
Thread.currentThread().setName("task-" + taskId.addAndGet(1));
showLogId();
logId.set(30);
showLogId();
}));
// 关闭线程池
executorService.shutdown();
// 等待至任务都执行完,最多等3秒
executorService.awaitTermination(3, TimeUnit.SECONDS);
showLogId();
}
}
运行结果:
main : null
main : 10
task-1 : null
task-1 : 20
task-2 : null
task-2 : 30
main : 10
可以看到
如何将 Threadlocal 传递到另外一个线程
传递到另外一个线程有两个场景:
- 传递给当前线程创建的新线程(也可以叫做当前线程的子线程)
- 传递给其他线程创建的新线程
但都是一样的处理。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class ThreadLocalTest {
private static ThreadLocal<Integer> logId = new ThreadLocal<>();
private static AtomicLong taskId = new AtomicLong(); // 生成任务编号
// 加强版 Runnable
public static class EnhancedRunnable implements Runnable {
private Runnable delegate;
private Integer logIdValue;
// 初始化是在当前线程中执行
public EnhancedRunnable(Runnable task) {
delegate = task;
logIdValue = logId.get();
}
public static EnhancedRunnable of(Runnable task) {
return new EnhancedRunnable(task);
}
// 下面的逻辑是在另外一个线程中执行
@Override
public void run() {
logId.set(logIdValue); // 将提交任务到本线程的线程logId值设置到本线程
delegate.run();
logId.remove(); // 清理 logId 的值
}
}
public static void showLogId() {
System.out.printf("%s : %s\n", Thread.currentThread().getName(), logId.get());
}
public static void main(String[] args) throws InterruptedException {
showLogId();
logId.set(10);
showLogId();
// 只有一个线程的线程池,该线程会复用
ExecutorService executorService = Executors.newFixedThreadPool(1);
// 提交任务
executorService.submit(EnhancedRunnable.of(() -> {
Thread.currentThread().setName("task-" + taskId.addAndGet(1));
showLogId();
logId.set(20);
showLogId();
}));
// 再提交一个任务
executorService.submit(EnhancedRunnable.of(() -> {
Thread.currentThread().setName("task-" + taskId.addAndGet(1));
showLogId();
logId.set(30);
showLogId();
}));
// 关闭线程池
executorService.shutdown();
// 等待至任务都执行完,最多等3秒
executorService.awaitTermination(3, TimeUnit.SECONDS);
showLogId();
}
}
执行结果:
main : null
main : 10
task-1 : 10
task-1 : 20
task-2 : 10
task-2 : 30
main : 10
可以看到,main线程在设置自己的logId为10之后,提交了两个任务到线程池,这两个任务在新线程执行时,logId的初始值都是10。
Java 本身提供了一个 ThreadLocal 的加强版实现:InheritableThreadLocal (Java InheritableThreadLocal)。可以让子线程继承父线程的值,但无法处理线程池中的复用问题。 alibaba 的 transmittable-thread-local 库 (Java alibaba transmittable-thread-local 库:让 ThreadLocal 跨线程传播)提供了子线程/线程池传递ThreadLocal的能力,设计的基本思路和上面示例类似。
ThreadLocal 原理
- Thread 类 和 ThreadLocal 类都在 java.lang 包中。
- ThreadLocal 本身不直接存储数据,它里面定义了 ThreadLocalMap 类 ,key是 ThreadLocal 对象,value是 ThreadLocal 对象对应的值。key和value被封装在 ThreadLocalMap.Entry 中,ThreadLocalMap.Entry 继承自 WeakReference,所以不会出现内存问题。
- 每个Thread对象都有一个 ThreadLocalMap 类型的实例变量 threadLocals 。
- 调用 ThreadLocal 变量(比如叫 logId)的get方法时,先拿到当前线程的 threadLocals 值。
- 若 threadLocals 为 null,则初始化该对象。
- 判断 threadLocals 中是否有 logId 为key 的数据;若有,则直接返回;若无,则 logId 和 对应的初始化值(一般是 null),放入threadLocals中,然后返回。
- 调用 ThreadLocal 变量(比如叫 logId)的set方法(例如设值为 123)时,先拿到当前线程的 threadLocals 值。
- 若 threadLocals 为 null,则初始化该对象。
- 将 logId 为key,123 作为value,放入 threadLocals 中。