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

Java 面试题:从源码理解 ThreadLocal 如何解决内存泄漏 ConcurrentHashMap 如何保证并发安全 --xunznux

文章目录

  • ThreadLocal
    • ThreadLocal 的基本原理
    • ThreadLocal 的实现细节
    • 内存泄漏
    • 源码
    • 使用场景
  • ConcurrentHashMap 怎么实现线程安全的
    • CAS初始化源码
    • 添加元素putVal方法

ThreadLocal

ThreadLocal 是 Java 中的一种用于在多线程环境下存储线程局部变量的机制,它可以为每个线程提供独立的变量副本,从而避免多个线程之间的竞争条件。ThreadLocal 在实际应用中,特别是在需要在线程间共享资源的场景下,发挥着重要作用。

ThreadLocal 的基本原理

ThreadLocal 的核心概念是为每个线程维护一个独立的变量副本。当一个线程通过 ThreadLocal 访问某个变量时,实际上访问的是属于该线程的独立副本。ThreadLocal 通过以下几个关键点实现了这一点:

  1. 每个线程持有自己的 ThreadLocalMap
    每个线程内部都有一个 ThreadLocalMap 对象,这个对象存储了 ThreadLocal 变量及其对应的值。ThreadLocalMap 是 Thread 类中的一个成员变量,因此它与线程的生命周期绑定。
  2. ThreadLocalMap** 的结构**:
    ThreadLocalMap 是一个定制的哈希表,它的键是 ThreadLocal 对象,而值是对应的线程局部变量的值。每个线程持有的 ThreadLocalMap 可以存储多个 ThreadLocal 变量。
  3. 变量的存取过程:
    • 当线程调用 ThreadLocal 的 get() 方法时,ThreadLocal 会获取当前线程持有的 ThreadLocalMap,然后通过自身(作为键)从 ThreadLocalMap 中获取变量的值。
    • 当线程调用 ThreadLocal 的 set() 方法时,ThreadLocal 会将变量的值存储到当前线程持有的 ThreadLocalMap 中。

ThreadLocal 的实现细节

  1. ThreadLocal
    ThreadLocal 本身只是提供了一套访问接口,它内部依赖于 ThreadLocalMap 来存储和获取线程局部变量。
  2. ThreadLocalMap的实现
    • ThreadLocalMap 是一个内部类,它的结构类似于一个简化的哈希表。ThreadLocalMap 使用了一个简单的开放地址法来处理哈希冲突。
    • 每个键值对的键是一个 ThreadLocal 的弱引用(WeakReference),这有助于避免内存泄漏:当 ThreadLocal 对象被回收后,键会变成 null,相应的值也会被清理。
  3. ThreadLocalMap的垃圾回收
    由于 ThreadLocalMap 使用了弱引用,ThreadLocal 对象不会阻止其被垃圾回收机制回收。当 ThreadLocal 对象被回收后,ThreadLocalMap 中对应的键会变成 null,但是值仍然会存在。这种情况下,如果不及时清理,可能会导致内存泄漏。
  4. remove()方法
    ThreadLocal 提供了一个 remove() 方法,可以显式地将当前线程持有的 ThreadLocal 变量移除。这有助于防止内存泄漏,特别是在使用线程池的场景下,线程会被重复利用,如果不清理,可能会导致数据污染或内存泄漏。

内存泄漏

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。

如何避免内存泄漏:

  • 每次使用完threadlocal调用remove方法清除
  • 尽可能把threadlocal变量定义为static final,这样可以避免频繁创建实例。
  • 内部优化
    • 调用set()方法,会采用采样清理,全量清理,扩容时还能继续检查
    • 调用get()方法,如果没有命中,向后环形查找时进行清理。
    • 调用remove()方法,清理当前entry,还会向后清理

最好是使用完之后手动调用remove方法,这个方法底层会调用map的remove将 Entry 移除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。弱引用只是保证了 ThreadLocal 会被 GC 自动回收。
最佳做法:每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

源码

static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}
}

该Java静态内部类Entry继承自WeakReference<ThreadLocal<?>>,主要用于存储ThreadLocal与其关联对象的弱引用及其对应的值。具体说明如下:
value:存储与ThreadLocal关联的具体值。
构造方法接收一个ThreadLocal对象和一个值,创建一个弱引用来持有ThreadLocal对象,并保存关联值。

使用场景

ThreadLocal 常用于以下场景:

  • 数据库连接管理:每个线程持有一个独立的数据库连接,避免多个线程同时使用同一个连接。
  • 用户上下文信息:在 Web 应用中,每个线程处理一个用户请求,可以通过 ThreadLocal 存储和访问该用户的上下文信息。
  • 线程安全的对象实例:通过 ThreadLocal 为每个线程创建独立的对象实例,避免线程间的竞争条件。
    注意事项
  • 内存泄漏:如果 ThreadLocal 变量不及时清理,可能会导致内存泄漏,尤其是在使用线程池时要特别注意调用 remove() 方法。
  • 适用场景:ThreadLocal 适合用于线程独立的数据存储,不适合跨线程的数据共享。
    总结来说,ThreadLocal 是一种通过在每个线程中创建独立变量副本的方式,来实现线程隔离的工具,它的底层依赖于每个线程持有的 ThreadLocalMap 来存储这些变量副本,从而确保线程间的数据独立性和安全性。

例子:
ThreadLocalMap 中的 Key 和 Value

  • Key:
    • 在 ThreadLocalMap 中,Key 是 ThreadLocal 实例本身。ThreadLocal 对象作为键,指向当前线程所持有的变量。
  • Value:
    • Value 是由 ThreadLocal 对象所关联的值。在本例中,Value 是 Integer,存储的是每个线程独立的计数器值。

每个线程都有一个 ThreadLocalMap,这个 ThreadLocalMap 使用 ThreadLocal 实例作为键来存储和获取对应的线程局部变量值,从而实现数据隔离。

public class MultipleThreadLocalExample {// 定义两个不同的 ThreadLocal 变量private static ThreadLocal<Integer> threadLocalCounter1 = ThreadLocal.withInitial(() -> 0);private static ThreadLocal<String> threadLocalCounter2 = ThreadLocal.withInitial(() -> "Initial Value");public static void main(String[] args) {// 启动一个线程,独立操作两个 ThreadLocal 变量Thread thread = new Thread(() -> {// 操作第一个 ThreadLocal 变量threadLocalCounter1.set(threadLocalCounter1.get() + 10);System.out.println(Thread.currentThread().getName() + " - Counter1: " + threadLocalCounter1.get());// 操作第二个 ThreadLocal 变量threadLocalCounter2.set(threadLocalCounter2.get() + " Updated");System.out.println(Thread.currentThread().getName() + " - Counter2: " + threadLocalCounter2.get());});thread.start();}
}

多个 ThreadLocal 变量在同一线程中的工作机制

  • ThreadLocalMap 的键区分:
    • 在同一个线程中,不同的 ThreadLocal 变量对应着不同的键。这些键就是 ThreadLocal 对象本身。因此,即使在同一个线程中,每个 ThreadLocal 实例都能独立存储和访问自己的值,不会与其他 ThreadLocal 变量发生冲突。
  • ThreadLocalMap 的存储结构:
    • ThreadLocalMap 通过一个哈希表来存储键值对。键是 ThreadLocal 对象,值是线程局部变量的实际数据。因此,同一线程中的多个 ThreadLocal 变量不会相互覆盖或混淆。

ConcurrentHashMap 怎么实现线程安全的

采用了CAS算法(compareAndSwapObject)和部分代码使用synchronized锁保证线程安全。
对应的非并发容器:HashMap
目标:代替Hashtable、synchronizedMap,支持复合操作。
原理:JDK6中采用一种更加细粒度的加锁机制 Segment “分段锁”,JDK8中采用 volatile + CAS 或者 synchronized 。
**添加元素时首先会判断容器是否为空:

  • 如果为空则使用 volatile 加 CAS 来初始化
  • 如果容器不为空,则根据存储的元素计算该位置是否为空。
    • 如果根据存储的元素计算结果为空,则利用 CAS 设置该节点;
    • 如果根据存储的元素计算结果不为空,则使用 synchronized ,然后,遍历桶中的数据,并替换或新增节点到桶中,最后再判断是否需要转为红黑树,这样就能保证并发访问时的线程安全了**。

如果把上面的执行用一句话归纳的话,就相当于是ConcurrentHashMap通过对头结点加锁来保证线程安全的,锁的粒度相比 Segment 来说更小了,发生冲突和加锁的频率降低了,并发操作的性能就提高了。
而且 JDK 1.8 使用的是红黑树优化了之前的固定链表,那么当数据量比较大的时候,查询性能也得到了很大的提升,从之前的 O(n) 优化到了 O(logn) 的时间复杂度。

CAS初始化源码

这段 Java 代码定义了一个名为 sizeCtl 的私有变量,其类型为 int,并且被 transient 和 volatile 修饰符所修饰。这个变量在类中的作用如下:
Table 初始化和调整大小控制:

  • 当 sizeCtl 的值为负数时,表示当前正在进行表的初始化或调整大小操作。
    • -1 表示正在初始化表。
    • 其他负数值(如 -2, -3 等)表示正在进行调整大小的操作,并且 -1 减去该值即为当前活跃的调整大小线程的数量。
      初始表大小或默认值:
  • 如果表(table)为空,并且 sizeCtl 的值为非负数,则该值表示创建表时应使用的初始大小。
  • 如果 sizeCtl 的值为 0,则表示使用默认大小创建表。
    调整大小的阈值:
  • 在初始化之后,sizeCtl 的值表示下次应该调整表大小时元素的数量阈值。
    变量修饰符说明:
  • transient:表示这个变量不会被序列化。当对象被序列化成字节流时,sizeCtl 的值不会被保存。
  • volatile:确保多线程环境下的可见性和有序性,即任何线程对 sizeCtl 的修改都会立即反映到其他线程中。
    通过这种方式,sizeCtl 变量帮助实现了并发哈希表(如 ConcurrentHashMap)在初始化和动态调整大小过程中的控制逻辑。
private transient volatile int sizeCtl;
private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;while ((tab = table) == null || tab.length == 0) {if ((sc = sizeCtl) < 0)Thread.yield(); // lost initialization race; just spin提示调度器当前线程愿意放弃处理器使用权else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {try {if ((tab = table) == null || tab.length == 0) {int n = (sc > 0) ? sc : DEFAULT_CAPACITY;@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = tab = nt;sc = n - (n >>> 2);}} finally {sizeCtl = sc;}break;}}return tab;
}

该函数初始化哈希表,主要功能如下:

  • 检查当前表格是否为空或长度为零。
  • 使用CAS操作安全地初始化表格数组。
  • 如果成功,则根据sizeCtl记录的大小创建新数组,并更新sizeCtl值。

添加元素putVal方法

    /*** 核心方法,用于处理put和putIfAbsent操作* 该方法实现了哈希表的插入逻辑,包括处理哈希冲突和数据结构转换(链表转红黑树)* * @param key 键,不能为null* @param value 值,不能为null* @param onlyIfAbsent 如果为true,则仅在键不存在时进行插入* @return 插入前该键对应的旧值,如果没找到则返回null*/final V putVal(K key, V value, boolean onlyIfAbsent) {// 检查键值对是否为null,为null则抛出异常if (key == null || value == null) throw new NullPointerException();// 扩散哈希码,以减少哈希冲突int hash = spread(key.hashCode());int binCount = 0; // 用于记录链表或红黑树中的元素数量// 循环尝试在哈希表中插入值for (Node<K,V>[] tab = table;;) {Node<K,V> first; int n, i, firstHash;// 表为空时初始化哈希表(volatile和CAS)if (tab == null || (n = tab.length) == 0)tab = initTable();// 位置i处的节点为空,直接插入新节点else if ((first = tabAt(tab, i = (n - 1) & hash)) == null) {// 存储的元素在该node数组的下标位置结果为空,则利用 CAS 设置该节点if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break; // 成功插入,跳出循环}// 位置i处的节点处于迁移中,帮助完成迁移else if ((firstHash = first.hash) == MOVED)tab = helpTransfer(tab, first);// 位置i处的节点正常,进行插入或更新操作else {V oldVal = null;// 同步锁,确保线程安全。存储的元素计算结果不为空,则使用 synchronizedsynchronized (first) {// 再次检查节点,防止并发修改if (tabAt(tab, i) == first) {// 链表形式,遍历链表找到键或插入新节点if (firstHash >= 0) {binCount = 1;for (Node<K,V> e = first;; ++binCount) {K ek;// 找到匹配的键,更新值并跳出循环if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {oldVal = e.val;if (!onlyIfAbsent)e.val = value;break;}Node<K,V> pred = e;// 链表末尾插入新节点if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key,value, null);break;}}}// 红黑树形式,调用红黑树的插入或更新方法else if (first instanceof TreeBin) {Node<K,V> p;binCount = 2;if ((p = ((TreeBin<K,V>)first).putTreeVal(hash, key,value)) != null) {oldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}}// 链表节点数超过阈值,转换为红黑树if (binCount != 0) {if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);// 找到旧值,返回旧值if (oldVal != null)return oldVal;break;}}}// 更新哈希表大小和修改次数addCount(1L, binCount);return null; // 未找到旧值,返回null}

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 深入解析Flink SQL:基本概念与高级应用
  • 生活杂记1
  • C#基础(6)值类型和引用类型
  • 【鸿蒙 HarmonyOS NEXT】使用EventHub进行数据通信
  • java 防重复提交
  • P2343 宝石管理系统
  • SpringBoot开发——整合MyBatis
  • 人工智能在C/C++中的应用
  • VitePress 自定义主题:打造专属文档网站
  • 数学建模笔记—— 整数规划和0-1规划
  • 避障小车—51单片机
  • 大数据技术体系架构
  • 为何家用无线路由器不能实现PROFINET通信?
  • EasyExcel 文件导出:表头与内容样式简单设置
  • 【Tools】什么是基座模型
  • 4月23日世界读书日 网络营销论坛推荐《正在爆发的营销革命》
  • Android组件 - 收藏集 - 掘金
  • Angular数据绑定机制
  • AzureCon上微软宣布了哪些容器相关的重磅消息
  • C学习-枚举(九)
  • Intervention/image 图片处理扩展包的安装和使用
  • k个最大的数及变种小结
  • Quartz实现数据同步 | 从0开始构建SpringCloud微服务(3)
  • Redash本地开发环境搭建
  • Spring Cloud中负载均衡器概览
  • Vue实战(四)登录/注册页的实现
  • 多线程事务回滚
  • 干货 | 以太坊Mist负责人教你建立无服务器应用
  • 关于extract.autodesk.io的一些说明
  • 官方解决所有 npm 全局安装权限问题
  • ------- 计算机网络基础
  • 开源中国专访:Chameleon原理首发,其它跨多端统一框架都是假的?
  • 利用阿里云 OSS 搭建私有 Docker 仓库
  • 前端之Sass/Scss实战笔记
  • 使用权重正则化较少模型过拟合
  • 树莓派 - 使用须知
  • 一加3T解锁OEM、刷入TWRP、第三方ROM以及ROOT
  • MiKTeX could not find the script engine ‘perl.exe‘ which is required to execute ‘latexmk‘.
  • puppet连载22:define用法
  • 如何正确理解,内页权重高于首页?
  • ​configparser --- 配置文件解析器​
  • ​RecSys 2022 | 面向人岗匹配的双向选择偏好建模
  • ​香农与信息论三大定律
  • ‌‌雅诗兰黛、‌‌兰蔻等美妆大品牌的营销策略是什么?
  • #13 yum、编译安装与sed命令的使用
  • #多叉树深度遍历_结合深度学习的视频编码方法--帧内预测
  • #绘制圆心_R语言——绘制一个诚意满满的圆 祝你2021圆圆满满
  • (html5)在移动端input输入搜索项后 输入法下面为什么不想百度那样出现前往? 而我的出现的是换行...
  • (Java岗)秋招打卡!一本学历拿下美团、阿里、快手、米哈游offer
  • (Oracle)SQL优化技巧(一):分页查询
  • (poj1.2.1)1970(筛选法模拟)
  • (附源码)SSM环卫人员管理平台 计算机毕设36412
  • (每日一问)基础知识:堆与栈的区别
  • (免费领源码)Java#ssm#MySQL 创意商城03663-计算机毕业设计项目选题推荐
  • (五)activiti-modeler 编辑器初步优化