Java多线程与高并发专题——ThreadLocal 是用来解决共享资源的多线程访问的问题吗?
引入
这是一个常见的面试问题,如果被问到了 ThreadLocal,则有可能在你介绍完它的作用、注意点等内容之后,再问你:ThreadLocal 是不是用来解决共享资源的多线程访问的呢?假如遇到了这样的问题,其思路一定要清晰。本文我们就来看下这个问题的答案。
关于ThreadLocal
我们还是先看看它的源码注释:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e. g., a user ID or Transaction ID).
For example, the class below generates unique identifiers local to each thread. A thread's id is assigned the first time it invokes ThreadId. get() and remains unchanged on subsequent calls.import java. util. concurrent. atomic. AtomicInteger; public class ThreadId { // Atomic integer containing the next thread ID to be assigned private static final AtomicInteger nextId = new AtomicInteger(0); // Thread local variable containing each thread's ID private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return nextId. getAndIncrement(); } }; // Returns the current thread's unique ID, assigning it if necessary public static int get() { return threadId. get(); } }
Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).
翻译:
此类提供线程局部变量。这些变量与普通变量的不同之处在于,每个访问它们(通过其get或set方法)的线程都有自己独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,用于将状态与线程相关联(例如,用户 ID 或事务 ID)。
例如,下面的类为每个线程生成局部唯一标识符。线程首次调用ThreadId.get()时会分配其 ID,后续调用时该 ID 保持不变。/** * 导入用于原子操作的AtomicInteger类 */ import java.util.concurrent.atomic.AtomicInteger; /** * 该类用于为每个线程分配唯一的ID。 * 它使用AtomicInteger来生成唯一的ID,并使用ThreadLocal来存储每个线程的ID。 */ public class ThreadId { /** * 原子整数,包含下一个要分配的线程ID。 * 初始值为0,每次分配ID时会自动递增。 */ private static final AtomicInteger nextId = new AtomicInteger(0); /** * 线程局部变量,包含每个线程的唯一ID。 * 每个线程首次访问时,会调用initialValue方法获取一个新的唯一ID。 */ private static final ThreadLocal<Integer> threadId = // 重写ThreadLocal的initialValue方法,用于初始化线程局部变量的值 new ThreadLocal<Integer>() { /** * 为每个线程初始化一个唯一的ID。 * 这个方法会在每个线程第一次调用threadId.get()时被调用。 * * @return 线程的唯一ID */ @Override protected Integer initialValue() { // 获取并递增nextId的值 return nextId.getAndIncrement(); } }; /** * 返回当前线程的唯一ID。 * 如果当前线程还没有分配ID,则会分配一个新的ID。 * * @return 当前线程的唯一ID */ public static int get() { // 获取当前线程的唯一ID return threadId.get(); } }
只要线程处于活动状态且 ThreadLocal 实例可访问,每个线程就会对其线程局部变量的副本持有一个隐式引用;在线程终止后,其持有的所有 ThreadLocal 实例副本都可进行垃圾回收(除非对这些副本还存在其他引用)。
ThreadLocal 主要通过为每个线程维护一个独立的变量副本,来实现多线程环境下的数据隔离。当一个线程访问 ThreadLocal 变量时,它实际上是在访问自己线程内部的副本,而不会与其他线程的副本发生冲突。具体来说,ThreadLocal 内部通过一个哈希表来存储每个线程的变量副本,线程在获取变量时,通过自己的线程 ID 从哈希表中找到对应的变量副本进行访问。
ThreadLocal 的主要用途
- 线程私有变量:为每个线程提供一个独立的变量副本,使得每个线程可以安全地修改自己的副本。
- 线程上下文:在多线程环境中,ThreadLocal 可以用来存储线程的上下文信息,如用户标识、事务标识等,这些信息需要在线程的整个生命周期中保持一致。
- 避免同步:通过为每个线程提供独立的变量副本,ThreadLocal 可以避免使用同步机制,从而提高性能。
如何回答
这道题的答案很明确——不是,ThreadLocal 并不是用来解决共享资源问题的。虽然 ThreadLocal 确实可以用于解决多线程情况下的线程安全问题,但其资源并不是共享的,而是每个线程独享的。所以这道题其实是有一定陷阱成分在内的。
ThreadLocal 解决线程安全问题的时候,相比于使用“锁”而言,换了一个思路,把资源变成了各线程独享的资源,非常巧妙地避免了同步操作。具体而言,它可以在 initialValue 中 new 出自己线程独享的资源,而多个线程之间,它们所访问的对象本身是不共享的,自然就不存在任何并发问题。这是ThreadLocal 解决并发问题的最主要思路。
如果我们把放到 ThreadLocal 中的资源用 static 修饰,让它变成一个共享资源的话,那么即便使用了ThreadLocal,同样也会有线程安全问题。
我们看如下代码案例:
public class ThreadLocalStatic {
// 创建一个固定大小为 16 的线程池,用于执行多个任务
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
// 定义一个静态的 SimpleDateFormat 实例,用于格式化日期
static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
/**
* 程序的入口点,创建并提交 1000 个任务到线程池,每个任务格式化一个日期并打印。
*
* @param args 命令行参数
* @throws InterruptedException 如果线程在等待时被中断
*/
public static void main(String[] args) throws InterruptedException {
// 循环 1000 次,为每个任务创建一个线程
for (int i = 0; i < 1000; i++) {
// 将循环变量 i 赋值给 final 变量 finalI,以便在匿名内部类中使用
int finalI = i;
// 向线程池提交一个新的任务
threadPool.submit(new Runnable() {
/**
* 线程执行的任务,调用 date 方法格式化日期并打印结果。
*/
@Override
public void run() {
// 调用 date 方法格式化日期
String date = new ThreadLocalStatic().date(finalI);
// 打印格式化后的日期
System.out.println(date);
}
});
}
// 关闭线程池,不再接受新任务
threadPool.shutdown();
}
/**
* 根据给定的秒数生成一个格式化的日期字符串。
*
* @param seconds 从纪元开始的秒数
* @return 格式化后的日期字符串
*/
public String date(int seconds) {
// 创建一个 Date 对象,根据给定的秒数
Date date = new Date(1000 * seconds);
// 从 ThreadLocal 中获取当前线程的 SimpleDateFormat 实例
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
// 使用 SimpleDateFormat 格式化日期
return dateFormat.format(date);
}
}
/**
* 该类提供了一个线程安全的 SimpleDateFormat 实例,通过 ThreadLocal 实现。
*/
class ThreadSafeFormatter {
// 定义一个 ThreadLocal 变量,用于为每个线程提供一个独立的 SimpleDateFormat 实例
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
/**
* 为每个线程初始化一个 SimpleDateFormat 实例。
*
* @return 初始化的 SimpleDateFormat 实例
*/
@Override
protected SimpleDateFormat initialValue() {
// 返回 ThreadLocalStatic 类中定义的静态 SimpleDateFormat 实例
return ThreadLocalStatic.dateFormat;
}
};
}
可以看到,我们在SimpleDateFormat 之前加上一个 static 关键字来修饰,并且把这个静态对象放到 ThreadLocal 中去存储,那么在多线程中去获取这个资源并且同时使用的话,会出现时间重复的问题,运行结果如下:
可以看出,00:00 被多次打印了,发生了线程安全问题。也就是说,如果我们需要放到 ThreadLocal 中的这个对象是共享的,是被 static 修饰的,那么此时其实根本就不需要用到 ThreadLocal,即使用了ThreadLocal 也并不能解决线程安全问题。
相反,我们对于这种共享的变量,如果想要保证它的线程安全,应该用其他的方法,比如说可以使用synchronized 或者是加锁等其他的方法来解决线程安全问题,而不是使用 ThreadLocal,因为这不是ThreadLocal 应该使用的场景。
这个问题回答到这里,通常会引申出下面这个问题。
ThreadLocal 和 synchronized 是什么关系
面试官可能会问:你既然说 ThreadLocal 和 synchronized 它们两个都能解决线程安全问题,那么
ThreadLocal 和 synchronized 是什么关系呢?
我们先说第一种情况。当 ThreadLocal 用于解决线程安全问题的时候,也就是把一个对象给每个线程都生成一份独享的副本的,在这种场景下,ThreadLocal 和 synchronized 都可以理解为是用来保证线程安全的手段。
但是它们的效果和实现原理不同:
- ThreadLocal 是通过为每个线程提供独立的变量副本来实现线程安全,避免了资源的竞争适用于每个线程需要独立维护自己的状态的场景。
- synchronized 主要用于临界资源的分配,通过锁机制来确保多个线程可以安全地访问和修改共享资源,在同一时刻限制最多只有一个线程能访问该资源,适用于多个线程需要协同修改共享资源的场景。
相比于 ThreadLocal 而言,synchronized 的效率会更低一些,但是花费的内存也更少。在这种场景下,ThreadLocal 和 synchronized 虽然有不同的效果,不过都可以达到线程安全的目的。
但是对于 ThreadLocal 而言,它还有不同的使用场景。比如当 ThreadLocal 用于让多个类能更方便地拿到我们希望给每个线程独立保存这个信息的场景下时(比如每个线程都会对应一个用户信息,也就是user 对象),在这种场景下,ThreadLocal 侧重的是避免传参,所以此时 ThreadLocal 和synchronized 是两个不同维度的工具。
ThreadLocal 与 synchronized 的核心区别:
特性 | ThreadLocal | synchronized |
---|---|---|
作用机制 | 线程隔离(每个线程独立副本) | 锁机制(共享资源互斥访问) |
性能 | 高(无锁) | 中低(可能阻塞线程) |
适用场景 | 线程私有数据(如用户 ID、事务上下文) | 共享资源同步(如计数器、全局变量) |
数据共享性 | 不共享 | 共享 |
典型问题 | 内存泄漏(静态 ThreadLocal) | 死锁、性能瓶颈 |