理解ThreadLocal 变量副本,为什么不同线程的 ThreadLocalMap互不干扰
创始人
2024-11-15 03:03:44
0

ThreadLocal 类在 Java 中提供了一种线程局部变量的存储方式,这种方式使得每个线程可以访问到自己的变量副本,而这个副本对于其他线程是不可见的。这听起来可能有些抽象,下面我将通过一个简单的例子来解释这个概念。

假设我们有一个简单的计数器,我们希望每个线程都可以拥有自己的计数器,并且每个线程增加计数器的值时不会影响其他线程的计数器。这时,我们可以使用 ThreadLocal 来实现:

public class Counter { 	// 静态成员变量     private static final ThreadLocal threadLocalCounter = ThreadLocal.withInitial(() -> 0);      public static void increment() {         // 获取当前线程的计数器副本,并递增         threadLocalCounter.set(threadLocalCounter.get() + 1);     }      public static int getCount() {         // 返回当前线程的计数器副本的值         return threadLocalCounter.get();     } } 

在这个例子中,我们定义了一个 Counter 类,它有一个静态的 ThreadLocal 类型的成员变量 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 {     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>,它是存储在 ThreadLocalMap 中的实际元素。 	 // 每个 Entry包含一个 ThreadLocal 的弱引用和一个对应的值。     static class Entry extends WeakReference> {         // 存储实际的值         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 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() 方法来清除线程局部变量,避免潜在的内存问题。

相关内容

热门资讯

透视脚本!hhpoker破解工... 透视脚本!hhpoker破解工具(底牌)详细透视辅助挂(有挂实操);1、hhpoker破解工具系统规...
六分钟普及!如何判断wpk辅助... 六分钟普及!如何判断wpk辅助软件的真假,wpk有辅助器,详细教程(有挂技巧)运如何判断wpk辅助软...
记者揭秘!poker辅助器免费... 记者揭秘!poker辅助器免费安装,wepoker辅助软件价格,解密教程(有挂黑科技)所有人都在同一...
透视脚本!hhpoker是真的... 透视脚本!hhpoker是真的还是假的(底牌)详细透视辅助工具(有挂秘笈);1、让任何用户在无需hh...
透视脚本!aapoker辅助插... 透视脚本!aapoker辅助插件工具(透视)详细破解侠是真的辅助神器(有挂教程);1、玩家可以在aa...
三分钟辅助挂!wpk模拟器,(... 三分钟辅助挂!wpk模拟器,(WPK)其实有挂(详细透视辅助插件详情)1、构建自己的wpk模拟器辅助...
第五分钟透视!wepoker透... 第五分钟透视!wepoker透视脚本视频,(wepoker)一贯真的有挂,黑科技教程(有挂详情)1、...
推荐一款!hh poker辅助... 推荐一款!hh poker辅助器先试用,hhpoker辅助挂,透明挂教程(有挂详情)hh poker...
透视脚本!aapoker怎么选... 透视脚本!aapoker怎么选牌(透视)详细安装包可以使用辅助脚本(有挂辅助)1、aapoker怎么...
1分钟透视!wejoker免费... 1分钟透视!wejoker免费脚本,(wepoker)一直是有挂,高科技教程(有挂教程)1、进入游戏...