ConcurrentHashMap学习

本文深入探讨了ConcurrentHashMap的设计理念及其实现机制,包括其如何通过分段锁提高并发性能,以及put、get和size操作的具体流程。

概述


ConcurrentHashMap绝对是编码的经典之作,里面的设计思想可以开拓我们的编程思路。想在高并发多线程环境使用一个Map的话,ConcurrentHashMap是首选,它不只性能高,还是线程安全的。

我们最常使用的HashMap是线程不安全的,可以参考HashMap多线程下发生死循环的原因


减小锁的粒度


HashTable虽然是线程安全的,但是所用的锁的粒度是容器级别的,在高并发的情况下,多个线程都在抢夺一把锁,抢不到锁的线程会被挂起,等着被再次调度,这样就有很多上下文切换的开销。

如果把Map里面的数据进行分段,每个段配置一把锁,在高并发的情况下,线程与线程之间修改的数据如果不是同一个段的,就可以各自修改各自段里面的数据。
这样多个线程就可以并发修改和访问不同段中的数据,无需获取容器级别的锁了,只需要获取段的锁
,大大提高了效率。

ConcurrentHashMap也是使用减小锁粒度这种方式来提高并发效率的。它将数据分段存储,每个段配置一把锁。


Segment段的定位


既然使用了分段存储数据的方式,那么无论是读取还是修改,第一步就是定位到对应的segment。ConcurrentHashMap会对key的hashcode进行再哈希,目的是减少hash冲突。然后使用下面的公式算出key对应的segment数组的下标:

(hash >>> segmentShift) & segmentMask
segment的长度设置

可以通过构造函数中的concurrencyLevel参数,并经过一定的算法得出segment的长度。

注意:如果Segment数组的长度太短,则带来更多的锁竞争,如果长度太长,则原本位于同一个Segment内的访问会扩散到不同的Segment中,会引起程序性能下降。


段里面的HashEntry数组中的元素定位


当成功的定位到segment后,还需要定位到segment中的Entry数组的下标。
segment里面存储的是一个HashEntry数组。

(table.length - 1) & hash

其中table指就是段中的HashEntry数组,使用上面的公式得到段中HashEntry数组的下标。无论是获取元素还是插入元素都是这样得到下标的。


数据结构图


这里写图片描述


put操作


1、先判断key对应的segment是否存在,如果不存在,则调用ensureSegment方法创建一个segment。

注意:ensureSegment方法在并发情况下可能有问题,但是该方法并没有使用加锁的办法来保证线程安全,而是结合CAS来保证创建一个segment的原子性。关于CAS的内容,可以参考学习一下CAS

2、 由于是写操作,先获取segment的锁,如果获取不到,不会使用互斥锁,而是使用自旋锁,尽量避免线程一开始就被挂起,先重试,一直到重试失败的次数到达阈值,当前线程才挂起。使用自旋锁可能是基于下面原因:

就算锁被其他线程获取了,也不会获取太久,很快会释放。

3、获取锁成功后,先判断是否有相同的key,如果有,则覆盖,没有则创建一个HashEntry,作为链表的头。如果segment里面的HashEntry的个数已经超过阈值,则进行段内的扩容(注意:不会对整个容器进行扩容)。
扩容的时候,为了不影响get操作,会创建新的HashEntry节点,旧的HashEntry节点仍然留着(有点copy-on-write的味道,请看线程安全的CopyOnWriteArrayList介绍)。
不过在理解Segment的扩容时,最好先了解一下HashMap的扩容流程,可以参考HashMap扩容

4、释放锁


get操作


get操作就比较简单了,有一点需要提的就是,整个get的过程是不需要加锁的,原因是HashEntry中的变量是volatile类型的:

volatile V value;
volatile HashEntry<K,V> next;

关于volatile的文章,可以参考学习volatile关键字


size操作


这个操作的实现也是有些难度的,原因是数据分段存储的,而且每个分段的元素也可能在不断的变化,ConcurrentHashMap使用如下思路进行处理:
1、首先不加锁循环所有的Segment,获得所有Segment的modcount之和。如果连续两次所有Segment的modcount和相等,则过程中没有发生其他线程修改ConcurrentHashMap的情况,返回获得的值。

2、如果modcount和上次不一样,则需要对所有segment加锁,然后进行统计。


参考的文章


  1. 深入分析ConcurrentHashMap
  2. ConcurrentHashMap之实现细节
  3. 构建一个更好的 HashMap
  4. 并发集合类
  5. 探索 ConcurrentHashMap 高并发性的实现机制
  6. Java Core系列之ConcurrentHashMap实现(JDK 1.7)
  7. ConcurrentHashMap总结
  8. ConcurrentHashMap实现原理——Java并发容器
<think>好的,我现在需要回答用户关于Java中ConcurrentHashMap的用法和最佳实践的问题。首先,我应该回顾一下ConcurrentHashMap的基本概念和它在Java中的应用场景。ConcurrentHashMap是Java并发包中的一个线程安全的哈希表实现,适合多线程环境下使用,以提高并发性能。 接下来,我需要确定用户可能关心的关键点。通常,使用ConcurrentHashMap时,需要注意其线程安全机制、如何正确地进行初始化、常用方法的使用,以及与HashMapHashtable的区别。此外,最佳实践可能涉及如何选择并发级别、处理null值的问题,以及迭代器的弱一致性特性。 然后,我应该检查用户提供的引用内容。引用[2]提到在多线程环境中使用WeakHashMap时,同步方法intern()可能成为竞争点,并建议在真实场景中使用ConcurrentHashMap。这说明ConcurrentHashMap在解决多线程同步问题上的优势。因此,在回答中需要强调ConcurrentHashMap线程安全实现,比如分段或CAS操作,以及如何减少竞争。 接下来,我需要组织内容结构。首先介绍ConcurrentHashMap的基本概念,然后详细说明其使用方法,包括初始化、常用方法、线程安全操作等。之后,列出最佳实践,比如避免不必要的竞争、合理设置并发级别、处理null值的注意事项等。最后,生成相关问题,帮助用户进一步学习。 在编写过程中,要确保符合用户提供的系统级指令,例如行内数学表达式使用$...$格式,但在这个问题中可能不太涉及数学公式,因此需要注意是否有相关部分。同时,引用标识需要自然地添加在引用的段落末尾,例如引用[2]提到使用ConcurrentHashMap代替同步的WeakHashMap,所以要在相应位置加上[^2]。 另外,用户要求回答结构清晰,逐步解决问题,所以需要分点说明,每个部分有明确的标题,如基本用法、最佳实践、常见问题等。同时,代码示例部分需要用正确的格式展示,比如Java代码块,帮助用户理解具体用法。 最后,生成的相关问题需要围绕ConcurrentHashMap展开,比如与HashMap的区别、线程安全机制、性能优化方法等,确保问题能够引导用户深入理解该主题。 在检查过程中,要确保LaTeX语法正确,但这个问题可能不需要数学表达式,所以主要检查代码块和格式是否正确。同时,使用中文回答,保持语言流畅自然,避免技术术语使用不当。</think>### Java中ConcurrentHashMap的用法与最佳实践 #### 1. ConcurrentHashMap的基本概念 `ConcurrentHashMap`是Java并发包(`java.util.concurrent`)中的线程安全哈希表实现,支持高并发读写操作。它通过分段(Java 7)或CAS操作+节点(Java 8及以后)实现高效并发,避免了全局的性能瓶颈。 #### 2. 基本用法 ##### 初始化 ```java ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); // 指定初始容量和负载因子 ConcurrentHashMap<String, Integer> map2 = new ConcurrentHashMap<>(16, 0.75f); ``` ##### 常用方法 ```java map.put("key", 100); // 线程安全的插入 int value = map.get("key"); // 线程安全的读取 map.computeIfAbsent("key", k -> 42); // 原子性操作:若不存在则计算并插入 map.forEach((k, v) -> System.out.println(k + ": " + v)); // 安全的遍历 ``` #### 3. 最佳实践 1. **减少竞争** - 合理设置`concurrencyLevel`(构造函数参数),默认值为16,需根据线程数调整。 - 使用`compute`、`merge`等原子方法替代“检查-写入”逻辑,避免显式同步。 2. **避免null值** `ConcurrentHashMap`不允许`null`作为键或值,需在代码中显式处理。 3. **弱一致性的迭代器** 遍历操作(如`entrySet().iterator()`)反映的是某一时刻的快照,不保证实时性。 4. **选择合适的并发版本** - Java 8+优化了粒度,优先使用新版本。 - 对于读多写少的场景,可结合`ReadWriteLock`或`ConcurrentHashMap`的原子操作。 5. **性能优化** ```java // 批量操作(并行阈值可调) map.search(1, (k, v) -> v > 100 ? k : null); map.reduce(1, (k, v) -> v, Integer::sum); ``` #### 4. 与HashMapHashtable的对比 | 特性 | ConcurrentHashMap | Hashtable | HashMap | |--------------------|-------------------|-----------------|-----------------| | 线程安全 | 是(分段/CAS) | 是(全局) | 否 | | 允许null键/值 | 否 | 否 | 是 | | 迭代器弱一致性 | 是 | 否 | 不适用 | #### 5. 常见问题 **Q: 如何保证复合操作的原子性?** 使用原子方法(如`compute`)或显式同步: ```java map.compute("counter", (k, v) -> v == null ? 1 : v + 1); ``` **Q: 多线程扩容是否会阻塞?** Java 8的`ConcurrentHashMap`通过多线程协同扩容,减少了阻塞时间。
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值