当前位置: 首页 > news >正文

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

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();// 获取当前线程的 ThreadLocalMapThreadLocalMap 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();// 获取当前线程的 ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {// 将值存储在 ThreadLocalMap 中map.set(this, value);} else {// 创建新的 ThreadLocalMapcreateMap(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,则返回该 Entryif (e != null && e.get() == key)return e;elsereturn 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() 方法来清除线程局部变量,避免潜在的内存问题。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • LSTM与GNN强强结合!全新架构带来10倍推理速度提升
  • centos7 中安装 mysql 8.x以及对数据库的管理(数据库、表的增删改查、插入删除数据)
  • Electron工作流程(2)——进程间通信
  • JavaScript青少年简明教程:面向对象编程入门
  • WEB服务器的详解与部署
  • 数学建模评价类模型—层次分析法(无数据情况下)
  • 解决VideoReader出现Thread worker: Error sending packet报错
  • Harmony-(2)-ArkTs
  • 精通Python爬虫中的XPath:从安装到实战演示
  • spring security和核心流程
  • KVM+GFS分布式存储系统构建KVM高可用
  • 【Python-MySQL】Python 代码用pool管理MySQL连接,并实现增删改查
  • Pip换源
  • 【zabbix6自定义监控带参数】
  • IIS解析漏洞
  • Angular数据绑定机制
  • axios请求、和返回数据拦截,统一请求报错提示_012
  • C++11: atomic 头文件
  • java8-模拟hadoop
  • SQLServer插入数据
  • vagrant 添加本地 box 安装 laravel homestead
  • VirtualBox 安装过程中出现 Running VMs found 错误的解决过程
  • Vue 重置组件到初始状态
  • vue--为什么data属性必须是一个函数
  • 将 Measurements 和 Units 应用到物理学
  • 算法之不定期更新(一)(2018-04-12)
  • 通信类
  • 推荐一个React的管理后台框架
  • 鱼骨图 - 如何绘制?
  • ​io --- 处理流的核心工具​
  • ​LeetCode解法汇总2182. 构造限制重复的字符串
  • ​批处理文件中的errorlevel用法
  • #android不同版本废弃api,新api。
  • #Datawhale AI夏令营第4期#AIGC文生图方向复盘
  • #我与Java虚拟机的故事#连载17:我的Java技术水平有了一个本质的提升
  • $NOIp2018$劝退记
  • (1)Jupyter Notebook 下载及安装
  • (2022版)一套教程搞定k8s安装到实战 | RBAC
  • (PySpark)RDD实验实战——取最大数出现的次数
  • (WSI分类)WSI分类文献小综述 2024
  • (二)Eureka服务搭建,服务注册,服务发现
  • (附源码)springboot高校宿舍交电费系统 毕业设计031552
  • (黑客游戏)HackTheGame1.21 过关攻略
  • (七)c52学习之旅-中断
  • (十三)Maven插件解析运行机制
  • (新)网络工程师考点串讲与真题详解
  • (转)AS3正则:元子符,元序列,标志,数量表达符
  • (转)C语言家族扩展收藏 (转)C语言家族扩展
  • (转)IIS6 ASP 0251超过响应缓冲区限制错误的解决方法
  • .net core 源码_ASP.NET Core之Identity源码学习
  • .NET 将混合了多个不同平台(Windows Mac Linux)的文件 目录的路径格式化成同一个平台下的路径
  • .net 流——流的类型体系简单介绍
  • .net 使用ajax控件后如何调用前端脚本
  • .NET简谈互操作(五:基础知识之Dynamic平台调用)
  • .net网站发布-允许更新此预编译站点