【数据结构】Java 常用集合类 ConcurrentHashMap(JDK 1.8)
整体设计
常用的 HashMap 是线程不安全的, Hashtable 是线程安全的。Hashtable 通过在方法上添加 synchronized
保证线程安全,相当于 Hashtable 实例只有一把锁,导致高并发场景下使用效率低。
JDK 1.7 ConcurrentHashMap 使用分段锁局部锁定的方式,ConcurrentHashMap 由多个 Segment(包含 Node 键值对)组成,每个 Segment 各自持有锁实现线程安全,当一个线程占用锁访问其中一个 Segment 数据的时候,其他 Segment 的数据能够被其他线程访问。对于一些需要跨多个 Segment 的方法(比如:size()
和 containsValue()
),需要锁整个表,这时会按顺序锁住所有 Segment,使用后再按顺序释放,防止死锁。读操作不加锁。
JDK 1.8 ConcurrentHashMap 使用了大量的 CAS 操作实现无锁操作,在少数地方(扩容操作、对某个桶中的 Node 操作...)用到 synchronized
同步锁保证线程安全。
ConcurrentHashMap 的实现基本上与 HashMap 类似,大部分属性可以参考 HashMap
构造函数
下面的构造函数指定了一个参数 concurrencyLevel
,表示能够同时更新 ConcurrentHashMap 且不产生锁竞争的最大线程数。
存储结构
Node 对象的实现与 HashMap.Node 类似(有细微区别),存储 key-value,需要注意的是 val 和 next 属性添加了 volatile
修饰保证可见性。并且 val 不允许修改(HashMap 中可以)。
当 Node 数量超过 TREEIFY_THRESHOLD
时,会由链表结构转换为红黑树 TreeNode。在 HashMap.TreeNode 中实现了 treeify
方法(链表转换为树),在 ConcurrentHashMap 中,转换的操作交给了 TreeBin 来实现。TreeBin 封装了对 TreeNode 的各种操作。
Hash 方法
首先比较一下 ConcurrentHashMap 与 HashMap 对 key 取哈希值的方法,有一些不同。
Get 方法
首先计算出 key 的 hashCode,找到 table 中的数组索引,即 Node<K, V> 对象,如果存在并且判断该结点是否等于要查询的节点,如果不是,按照链表或者红黑树的查询方式查找。
Put 方法
Put 操作应用到 CAS 算法,调用了形如 compareAndSwapXXX
的 native 方法实现。
初始化 table
扩容
在 treeifyBin 的操作中会判断是否真的需要转换树,还是对 table 进行扩容。当一个 table 的某个位置上元素比较多时,很大原因并不是因为这些 key 的 hashCode 相同,而是因为 table 比较小(跟 HashMap 的逻辑相同),hashCode 对 table 大小取摸后相同的概率比较大,所以选择对 table 进行扩容。扩容代码如下:
在 put 操作中,有一个判断调用了 helpTransfer
方法,进行帮助扩容,含义是有其它线程正在扩容时,当前线程一起转移元素。
扩容操作 transfer
相当复杂,抽取部分核心代码(添加锁的部分):
遍历方式
ConcurrentHashMap 是支持在遍历的时候,进行修改。但是频繁的修改和遍历 ConcurrentHashMap 还是会出问题,有可能遍历不到最新修改的数据。
已知问题
JDK7 HashMap 在多线程环境下可能造成 CPU 100% 的现象,由于在扩容的时候 put 时产生了环状链表,会在 get 时造成了 CPU 100%,在 JDK8 通过修改重新离散的方法得到解决。
JDK8 ConcurrentHashMap 也有一种情况会造成 CPU 100% 中,在 JDK9 中已经得到修复([https://bugs.openjdk.java.net/browse/JDK-8062841])
什么情况下 JDK8 ConcurrentHashMap 会出现这个问题?
问题的关键在于递归使用了 computeIfAbsent
方法,不要在递归中使用,具体的原因这里不再分析,避免使用。
与 Collections.synchronizedMap() 的区别
Collections.synchronizedMap()
方法来获取一个线程安全的集合(实现原理是 Collections 定义了一个 SynchronizedMap 的内部类,这个类实现了 Map 接口,在调用方法时使用 synchronized 来保证线程同步,其它 Collections.synchronizedXX 方法也是类似原理),所以 Collections.synchronizedMap() 更类似与 Hashtable,对 Map 进行同步。
还有一个区别是,ConcurrentHashMap 必然是个 HashMap。而 Collections.synchronizedMap() 可以接收任意 Map 实例,实现同步。
版权声明: 本文为 InfoQ 作者【Alex🐒】的原创文章。
原文链接:【http://xie.infoq.cn/article/5a0469f5e3cc661821ab4082b】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论