ConcurrentHashMap为什么支持完全并发的读

本文深入解析ConcurrentHashMap的实现原理,包括其如何支持并发读写操作而不需要加锁,HashEntry的具体结构及其不可变性特点,value域的volatile属性如何确保多线程环境下的一致性读取等。

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

ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。(事实上,ConcurrentHashMap支持完全并发的读以及一定程度并发的写。)如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。但是ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry代表每个hash链中的一个节点,其结构如下所示: 

static final class HashEntry<K,V> {
        final K key;
        final int hash;
        volatile V value;
        final HashEntry<K,V> next;

        HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
            this.key = key;
            this.hash = hash;
            this.next = next;
            this.value = value;
        }

	@SuppressWarnings("unchecked")
	static final <K,V> HashEntry<K,V>[] newArray(int i) {
	    return new HashEntry[i];
	}
    }

可以看到除了value不是final的,其它值都是final的,这意味着不能从hash链的中间或尾部添加或删除节点,因为这需要修改next引用值,所有的节点的修改只能从头部开始。对于put操作,可以一律添加到Hash链的头部。但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制一遍,最后一个节点指向要删除结点的下一个结点。为了确保读操作能够看到最新的值,将value设置成volatile,这避免了加锁。 remove操作要注意一个问题:如果某个读操作在删除时已经定位到了旧的链表上,那么此操作仍然将能读到数据,只不过读取到的是旧数据而已,这在多线程里面是没有问题的。
HashEntry 类的 value 域被声明为 Volatile 型,Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程“看”到。在 ConcurrentHashMap 中,不允许用 null作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。 

在看源码实现时,对HashEntry 的 value 域的值可能为 null有些疑惑,网上都是说发生了重排序现象,后来仔细想想不完全正确,重排序发生在删除操作时,这只是其中的一个原因,尽管ConcurrentHashMap不允许将value为null的值加入,但现在仍然能够读到一个为空的value就意味着此值对当前线程还不可见,主要因为HashEntry还没有完全构造完成导致的,所以对添加和删除(对链表的结构性修改都可能会导致value为null)。


ps:Hashtable,Collections.synchronizedMap,ConcurrentHashMap的实现的差异可参考http://hellosure.iteye.com/blog/1143942

### Java 1.7 中 `ConcurrentHashMap` 的文档与使用 #### 类概述 `ConcurrentHashMap` 是一种线程安全的哈希表实现,在多线程环境中提供了高效的并发访问能力。相比于传统的同步集合类如 `Hashtable` 或者通过外部加锁的方式保护 `HashMap`,`ConcurrentHashMap` 提供了更好的性能和伸缩性。 #### 主要特性 - **高并发度**:允许多个操作以及一定数量的同时入操作而不需要完全锁定整个映射。 - **分段锁机制**:内部采用分段(Segment)来存储数据,默认情况下有16个分段,每个分段相当于一个小的哈希表并有自己的锁对象[^1]。 #### 基本方法 以下是 `ConcurrentHashMap` 支持的一些常用的方法: - `put(K key, V value)`:如果指定键不存在,则将其关联到给定值;否则更新该键对应的旧值。 - `get(Object key)` :返回指定键所映射的值;如果没有找到则返回null。 - `remove(Object key)` : 移除由key指定条目。 - `containsKey(Object key)` : 判断是否存在某个特定的键。 - `size()` : 获取当前容器内元素的数量。 - `isEmpty()` : 测试此映射是否为空。 #### 使用示例 下面是一个简单的例子展示了如何创建和操作 `ConcurrentHashMap` 实例: ```java import java.util.concurrent.ConcurrentHashMap; public class ConcurrentHashMapExample { public static void main(String[] args) throws InterruptedException { // 创建一个新的 ConcurrentHashMAp 对象 ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); // 添加一些初始项 map.put("one", 1); map.put("two", 2); System.out.println(map.get("one")); // 输出: 1 // 并发修改map中的值 Thread t1 = new Thread(() -> { for(int i=0;i<5;i++){ map.put(Integer.toString(i),i*2); } }); Thread t2 = new Thread(() -> { for(int j=0;j<5;j++){ map.remove(Integer.toString(j)); } }); t1.start(); t2.start(); t1.join(); t2.join(); // 打印最终的结果集 System.out.println(map); } } ``` 这段代码首先初始化了一个 `ConcurrentHashMap` ,接着启动两个线程分别向其中添加和删除项目。由于 `ConcurrentHashMap` 内置的支持,即使是在多线程环境下也能正常工作而不必担心竞态条件等问题的发生[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值