理解ThreadLocal 变量副本,为什么不同线程的 ThreadLocalMap互不干扰

avatar
作者
猴君
阅读量:0

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 类

ThreadLocalMapThreadLocal 的内部类,它是一个定制化的 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(); 	} } 

工作机制

  1. 创建 ThreadLocal 对象

    • 当创建一个 ThreadLocal 对象时,并不会立即创建存储空间,只有在调用 get()set() 方法时,才会触发存储空间的创建。
  2. 调用 set() 方法

    • 当调用 ThreadLocalset() 方法时,当前线程会将该 ThreadLocal 对象和对应的值存储在自己的 ThreadLocalMap 中。ThreadLocalMap 是一个定制的 HashMap,它将 ThreadLocal 对象作为键,实际的值作为值存储。
  3. 调用 get() 方法

    • 当调用 ThreadLocalget() 方法时,会从当前线程的 ThreadLocalMap 中查找对应的值。如果找不到,则调用 initialValue() 方法来初始化该值。
  4. 每个线程独立存储

    • 每个线程都有自己的 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 的一些典型使用场景:

  1. 数据库连接和会话管理
    在 JDBC 或 JPA 等数据库访问框架中,ThreadLocal 可以用来存储每个线程的数据库连接或事务,确保线程安全和数据隔离。

  2. Web会话管理
    在 Web 应用中,ThreadLocal 可以用于存储会话信息,如购物车、用户偏好等,以便在请求处理过程中使用。。

  3. 日志记录
    ThreadLocal 可用于存储日志记录器的上下文信息,如日志级别、请求ID等,以便跨多个方法调用保持一致性。

  4. 资源隔离
    在多线程环境中,使用 ThreadLocal 可以为每个线程分配独立的资源,如缓存、临时变量等,避免资源冲突。

  5. 跟踪请求或事务
    在分布式系统中,ThreadLocal 可以用来跟踪请求或事务的生命周期,确保跨多个服务调用的一致性。

使用 ThreadLocal 时需要注意,它可能会导致内存泄漏,特别是在 Web 应用或应用服务器环境中,因为 ThreadLocal 对象如果没有被正确地清理,它们的值可能会长时间保留在内存中。因此,应当在适当的时候调用 ThreadLocal.remove() 方法来清除线程局部变量,避免潜在的内存问题。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!