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

本文主要是介绍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}

这篇关于Java 面试题:从源码理解 ThreadLocal 如何解决内存泄漏 ConcurrentHashMap 如何保证并发安全 --xunznux的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1141825

相关文章

springboot3.4和mybatis plus的版本问题的解决

《springboot3.4和mybatisplus的版本问题的解决》本文主要介绍了springboot3.4和mybatisplus的版本问题的解决,主要由于SpringBoot3.4与MyBat... 报错1:spring-boot-starter/3.4.0/spring-boot-starter-

Spring Boot整合消息队列RabbitMQ的实现示例

《SpringBoot整合消息队列RabbitMQ的实现示例》本文主要介绍了SpringBoot整合消息队列RabbitMQ的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的... 目录RabbitMQ 简介与安装1. RabbitMQ 简介2. RabbitMQ 安装Spring

springMVC返回Http响应的实现

《springMVC返回Http响应的实现》本文主要介绍了在SpringBoot中使用@Controller、@ResponseBody和@RestController注解进行HTTP响应返回的方法,... 目录一、返回页面二、@Controller和@ResponseBody与RestController

JAVA集成本地部署的DeepSeek的图文教程

《JAVA集成本地部署的DeepSeek的图文教程》本文主要介绍了JAVA集成本地部署的DeepSeek的图文教程,包含配置环境变量及下载DeepSeek-R1模型并启动,具有一定的参考价值,感兴趣的... 目录一、下载部署DeepSeek1.下载ollama2.下载DeepSeek-R1模型并启动 二、J

springboot rocketmq配置生产者和消息者的步骤

《springbootrocketmq配置生产者和消息者的步骤》本文介绍了如何在SpringBoot中集成RocketMQ,包括添加依赖、配置application.yml、创建生产者和消费者,并展... 目录1. 添加依赖2. 配置application.yml3. 创建生产者4. 创建消费者5. 使用在

Spring Retry 实现乐观锁重试实践记录

《SpringRetry实现乐观锁重试实践记录》本文介绍了在秒杀商品SKU表中使用乐观锁和MybatisPlus配置乐观锁的方法,并分析了测试环境和生产环境的隔离级别对乐观锁的影响,通过简单验证,... 目录一、场景分析 二、简单验证 2.1、可重复读 2.2、读已提交 三、最佳实践 3.1、配置重试模板

Spring中@Lazy注解的使用技巧与实例解析

《Spring中@Lazy注解的使用技巧与实例解析》@Lazy注解在Spring框架中用于延迟Bean的初始化,优化应用启动性能,它不仅适用于@Bean和@Component,还可以用于注入点,通过将... 目录一、@Lazy注解的作用(一)延迟Bean的初始化(二)与@Autowired结合使用二、实例解

SpringBoot使用Jasypt对YML文件配置内容加密的方法(数据库密码加密)

《SpringBoot使用Jasypt对YML文件配置内容加密的方法(数据库密码加密)》本文介绍了如何在SpringBoot项目中使用Jasypt对application.yml文件中的敏感信息(如数... 目录SpringBoot使用Jasypt对YML文件配置内容进行加密(例:数据库密码加密)前言一、J

Java中有什么工具可以进行代码反编译详解

《Java中有什么工具可以进行代码反编译详解》:本文主要介绍Java中有什么工具可以进行代码反编译的相关资,料,包括JD-GUI、CFR、Procyon、Fernflower、Javap、Byte... 目录1.JD-GUI2.CFR3.Procyon Decompiler4.Fernflower5.Jav

Spring Boot 中正确地在异步线程中使用 HttpServletRequest的方法

《SpringBoot中正确地在异步线程中使用HttpServletRequest的方法》文章讨论了在SpringBoot中如何在异步线程中正确使用HttpServletRequest的问题,... 目录前言一、问题的来源:为什么异步线程中无法访问 HttpServletRequest?1. 请求上下文与线