ThreadLocal
类在 Java 中提供了一种线程局部变量的存储方式,这种方式使得每个线程可以访问到自己的变量副本,而这个副本对于其他线程是不可见的。这听起来可能有些抽象,下面我将通过一个简单的例子来解释这个概念。
假设我们有一个简单的计数器,我们希望每个线程都可以拥有自己的计数器,并且每个线程增加计数器的值时不会影响其他线程的计数器。这时,我们可以使用 ThreadLocal
来实现:
public class Counter { // 静态成员变量 private static final ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 0); public static void increment() { // 获取当前线程的计数器副本,并递增 threadLocalCounter.set(threadLocalCounter.get() + 1); } public static int getCount() { // 返回当前线程的计数器副本的值 return threadLocalCounter.get(); } }
在这个例子中,我们定义了一个 Counter
类,它有一个静态的 ThreadLocal<Integer>
类型的成员变量 threadLocalCounter
。这个 ThreadLocal
对象负责为每个线程创建和存储一个独立的 Integer
类型的副本。
threadLocalCounter.withInitial(() -> 0)
这行代码创建了一个ThreadLocal
实例,并指定了一个初始化器,用于在线程首次访问时初始化副本的值(在这个例子中初始化为 0)。increment()
方法通过调用threadLocalCounter.get()
获取当前线程的计数器副本,并将其值加一,然后通过threadLocalCounter.set()
将更新后的值设置回当前线程的副本。getCount()
方法返回当前线程计数器副本的值。
现在,如果有多个线程调用 Counter.increment()
方法,每个线程都会操作自己的计数器副本,互不影响。这就是 ThreadLocal
的核心优势:提供了线程隔离的变量副本。
下面是一个使用 Counter
类的多线程示例:
public class ThreadLocalExample { public static void main(String[] args) { Thread thread1 = new Thread(Counter::increment); Thread thread2 = new Thread(Counter::increment); thread1.start(); thread2.start(); try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Count by thread 1: " + Counter.getCount()); // 输出将显示 "Count by thread 1: 1",因为 thread1 只增加了一次计数器 System.out.println("Count by thread 2: " + Counter.getCount()); // 输出将显示 "Count by thread 2: 1",因为 thread2 也只增加了一次计数器 // 注意:这里两次调用 getCount() 将返回不同线程的计数器副本的值 } }
在这个示例中,两个线程分别调用 increment()
方法,每个线程都会操作自己的计数器副本,因此最终输出的值都是 1,而不是 2。这说明 ThreadLocal
确实为每个线程提供了独立的变量副本。
ThreadLocal 的实现机制
ThreadLocal 类
ThreadLocal
类本身非常简单,主要的方法是 get()
和 set()
。
public class ThreadLocal<T> { public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的 ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { // 获取 ThreadLocalMap 中对应的值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T) e.value; return result; } } // 如果不存在,则初始化值 return setInitialValue(); } public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的 ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { // 将值存储在 ThreadLocalMap 中 map.set(this, value); } else { // 创建新的 ThreadLocalMap createMap(t, value); } } }
Thread 类
在 Thread
类中,有一个成员变量 threadLocals
,它是 ThreadLocal.ThreadLocalMap
类型。每个线程都有自己的 Thread
对象实例,因此每个线程都有自己的 threadLocals
成员变量。
public class Thread { // 用于存储线程的局部变量 ThreadLocal.ThreadLocalMap threadLocals = null; }
ThreadLocalMap 类
ThreadLocalMap
是 ThreadLocal
的内部类,它是一个定制化的 HashMap
,专门用于存储 ThreadLocal
的副本。
static class ThreadLocalMap { // ThreadLocalMap.Entry 继承自 WeakReference<ThreadLocal<?>>,它是存储在 ThreadLocalMap 中的实际元素。 // 每个 Entry包含一个 ThreadLocal 的弱引用和一个对应的值。 static class Entry extends WeakReference<ThreadLocal<?>> { // 存储实际的值 Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // 存储实际数据的数组 private Entry[] table; private Entry getEntry(ThreadLocal<?> key) { // 计算哈希值并取模获取数组索引 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; // 如果索引处的 Entry 存在且其键等于给定的 key,则返回该 Entry if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); } private void set(ThreadLocal<?> key, Object value) { // 计算哈希值并取模获取数组索引 int i = key.threadLocalHashCode & (table.length - 1); // 遍历该索引处的链表 for (Entry e = table[i]; e != null; e = table[nextIndex(i, table.length)]) { ThreadLocal<?> k = e.get(); if (k == key) { // 如果找到相同的 ThreadLocal 键,更新其值 e.value = value; return; } if (k == null) { // 如果找到无效的(被垃圾回收的)Entry,替换它 replaceStaleEntry(key, value, i); return; } } // 如果索引处没有找到相同的 ThreadLocal 键,新建一个 Entry 并插入 table[i] = new Entry(key, value); int sz = ++size; // 如果需要,清理一些槽位并检查是否需要扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } }
工作机制
创建
ThreadLocal
对象:- 当创建一个
ThreadLocal
对象时,并不会立即创建存储空间,只有在调用get()
或set()
方法时,才会触发存储空间的创建。
- 当创建一个
调用
set()
方法:- 当调用
ThreadLocal
的set()
方法时,当前线程会将该ThreadLocal
对象和对应的值存储在自己的ThreadLocalMap
中。ThreadLocalMap
是一个定制的HashMap
,它将ThreadLocal
对象作为键,实际的值作为值存储。
- 当调用
调用
get()
方法:- 当调用
ThreadLocal
的get()
方法时,会从当前线程的ThreadLocalMap
中查找对应的值。如果找不到,则调用initialValue()
方法来初始化该值。
- 当调用
每个线程独立存储:
- 每个线程都有自己的
ThreadLocalMap
,存储着各自的ThreadLocal
副本。不同线程的ThreadLocalMap
互不干扰。
- 每个线程都有自己的
示例代码
以下是一个完整的示例代码,演示了 ThreadLocal
的使用和工作机制:
public class ThreadLocalExample { private static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 1); public static void main(String[] args) { Runnable task = () -> { System.out.println(Thread.currentThread().getName() + " initial value: " + threadLocalValue.get()); threadLocalValue.set(threadLocalValue.get() + 1); System.out.println(Thread.currentThread().getName() + " updated value: " + threadLocalValue.get()); }; Thread thread1 = new Thread(task, "Thread 1"); Thread thread2 = new Thread(task, "Thread 2"); thread1.start(); thread2.start(); } }
运行结果
Thread 1 initial value: 1 Thread 2 initial value: 1 Thread 1 updated value: 2 Thread 2 updated value: 2
从输出结果可以看出,每个线程都有自己的 ThreadLocal
副本,互不干扰。这就是 ThreadLocal
提供线程隔离的核心机制。
ThreadLocal
的一些典型使用场景:
数据库连接和会话管理:
在 JDBC 或 JPA 等数据库访问框架中,ThreadLocal
可以用来存储每个线程的数据库连接或事务,确保线程安全和数据隔离。Web会话管理:
在 Web 应用中,ThreadLocal
可以用于存储会话信息,如购物车、用户偏好等,以便在请求处理过程中使用。。日志记录:
ThreadLocal
可用于存储日志记录器的上下文信息,如日志级别、请求ID等,以便跨多个方法调用保持一致性。资源隔离:
在多线程环境中,使用ThreadLocal
可以为每个线程分配独立的资源,如缓存、临时变量等,避免资源冲突。跟踪请求或事务:
在分布式系统中,ThreadLocal
可以用来跟踪请求或事务的生命周期,确保跨多个服务调用的一致性。
使用 ThreadLocal
时需要注意,它可能会导致内存泄漏,特别是在 Web 应用或应用服务器环境中,因为 ThreadLocal
对象如果没有被正确地清理,它们的值可能会长时间保留在内存中。因此,应当在适当的时候调用 ThreadLocal.remove()
方法来清除线程局部变量,避免潜在的内存问题。