currenthashmap线程安全的原因

本文详细分析了ConcurrentHashMap的线程安全策略,包括其延迟初始化、线程安全的put操作以及扩容机制。在初始化时,通过sizeCtl确保线程安全;put操作利用头节点锁保证并发插入;扩容过程中新建双倍大小的hash表并逐个链表迁移,避免了HashMap的并发问题。整个过程结合了synchronized、volatile和CAS保证线程安全。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 线程安全的hash表初始化

由上文可知ConcurrentHashMap是用table这个成员变量来持有hash表的。

table的初始化采用了延迟初始化策略,他会在第一次执行put的时候初始化table。

可通过sizeCtl保证线程安全。

成员变量sizeCtl在ConcurrentHashMap中的其中一个作用相当于HashMap中的threshold,当hash表中元素个数超过sizeCtl时,触发扩容; 他的另一个作用类似于一个标识,例如,当他等于-1的时候,说明已经有某一线程在执行hash表的初始化了,一个小于-1的值表示某一线程正在对hash表执行resize。

这个方法首先判断sizeCtl是否小于0,如果小于0,直接将当前线程变为就绪状态的线程。

当sizeCtl大于等于0时,当前线程会尝试通过CAS的方式将sizeCtl的值修改为-1。修改失败的线程会进入下一轮循环,判断sizeCtl<0了,被yield住;修改成功的线程会继续执行下面的初始化代码。

在new Node[]之前,要再检查一遍table是否为空,这里做双重检查的原因在于,如果另一个线程执行完#1代码后挂起,此时另一个初始化的线程执行完了#6的代码,此时sizeCtl是一个大于0的值,那么再切回这个线程执行的时候,是有可能重复初始化的。关于这个问题会在下图的并发场景中说明。

然后初始化hash表,并重新计算sizeCtl的值,最终返回初始化好的hash表。

下图详细说明了几种可能导致重复初始化hash表的并发场景,我们假设Thread2最终成功初始化hash表。 Thread1模拟的是CAS更新sizeCtl变量的并发场景 Thread2模拟的是table的双重检查的必要性

 

线程安全的put

put操作可分为以下两类 

当前hash表对应当前key的index上没有元素时 

当前hash表对应当前key的index上已经存在元素时(hash碰撞)

JDK8中直接用链表的头节点做为锁,ConcurrentHashMap通过这个锁的方式,使同一时间只有有一个线程对某一链表执行put,解决了并发问题。

线程安全的扩容

put方法的最后一步是统计hash表中元素的个数,如果超过sizeCtl的值,触发扩容。

扩容的代码略长,可大致看一下里面的中文注释,再参考下面的分析。 其实我们主要的目的是弄明白ConcurrentHashMap是如何解决HashMap的并发问题的。 带着这个问题来看源码就好。关于HashMap存在的问题,参考本文一开始说的笔者的另一篇文章即可。

其实HashMap的并发问题多半是由于put和扩容并发导致的。

这里我们就来看一下ConcurrentHashMap是如何解决的。

根据上述代码,对ConcurrentHashMap是如何解决HashMap并发问题这一疑问进行简要说明。

  • 首先new一个新的hash表(nextTable)出来,大小是原来的2倍。后面的rehash都是针对这个新的hash表操作,不涉及原hash表(table)。
  • 然后会对原hash表(table)中的每个链表进行rehash,此时会尝试获取头节点的锁。这一步就保证了在rehash的过程中不能对这个链表执行put操作。
  • 通过sizeCtl控制,使扩容过程中不会new出多个新hash表来。
  • 最后,将所有键值对重新rehash到新表(nextTable)中后,用nextTable将table替换。这就避免了HashMap中get和扩容并发时,可能get到null的问题。
  • 在整个过程中,共享变量的存储和读取全部通过volatile或CAS的方式,保证了线程安全

总结:

currenthashmap的线程安全保证主要通过synchronized,volatile,cas三种机制共同处理来保证整体的线程安全,线程安全问题主要出在put和扩容两方面。

put的时候会保证当前数组在该列的hash处已经锁定,并且没有出在扩容的前提下进行put,保证只有一个线程在该列put

扩容问题是通过cas和锁老保证的,首先会new出一个二倍大小的数组rehash不涉及原来数组,且对扩容列加锁并且可以实现并发扩容,将以扩完的列设置为不可用,最后都扩容完之后将新数组替换到table处。

### Java 中实现 `ConcurrentHashMap` 的元素过期机制 Java 提供的 `ConcurrentHashMap` 是一种高效的线程安全哈希表,但它本身并不支持内置的键值对过期功能。为了实现在一定时间之后自动移除某个键值对的功能,可以通过以下几种方式扩展其行为。 #### 方法一:通过自定义包装类 可以创建一个包装类,在其中存储键值对以及它们对应的最后访问时间或创建时间。当尝试获取某个键的值时,先检查该键是否已经超出了设定的时间范围,如果超出则删除它。 ```java import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; public class ExpiringMap<K, V> { private final long ttl; // Time to live in milliseconds private final ConcurrentHashMap<K, Entry<V>> map = new ConcurrentHashMap<>(); public ExpiringMap(long timeToLiveInSeconds) { this.ttl = TimeUnit.SECONDS.toMillis(timeToLiveInSeconds); } public void put(K key, V value) { map.put(key, new Entry<>(value, System.currentTimeMillis())); } public V get(K key) { Entry<V> entry = map.get(key); if (entry != null && !isExpired(entry)) { return entry.getValue(); } else { map.remove(key); // Remove expired entries return null; } } private boolean isExpired(Entry<?> entry) { return (System.currentTimeMillis() - entry.getTimestamp()) > ttl; } static class Entry<T> { private final T value; private final long timestamp; public Entry(T value, long timestamp) { this.value = value; this.timestamp = timestamp; } public T getValue() { return value; } public long getTimestamp() { return timestamp; } } } ``` 此方法利用了一个内部静态类 `Entry` 来保存实际的数据及其创建时间戳[^1]。每次调用 `get()` 或者其他可能涉及读取的操作之前都会验证这个条目是否仍然有效。 #### 方法二:借助定时器清理任务 另一种常见的做法是引入后台线程定期扫描整个映射表,并清除那些已经超过规定期限未被使用的记录。这种方式虽然简单易懂,但在高并发场景下可能会带来额外开销,因为它需要周期性地锁定部分区域进行迭代处理。 对于这种方法需要注意两点: 1. 定时器频率的选择应该平衡资源消耗和及时性的需求; 2. 清理过程中要小心避免死锁或者竞态条件的发生。 由于上述原因,通常推荐使用更轻量级的第一种方案除非有特殊理由才考虑第二种。 另外值得注意的是,在某些特定情况下还可以结合第三方库如 Guava Cache 或 Caffeine ,这些工具提供了更加灵活丰富的配置选项来满足不同的业务需求[^2]。 #### 总结 以上两种主要途径都可以有效地为 `ConcurrentHashMap` 添加基于时间维度上的约束能力即所谓的“过期”特性。开发者可以根据具体应用场景选取最适合自己的解决方案。 ```java // Example usage of the custom expiring map. ExpiringMap<String, String> cache = new ExpiringMap<>(30); // Entries expire after 30 seconds. cache.put("key", "value"); String result = cache.get("key"); // This will fetch 'value' within expiration window or null otherwise. ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值