一、 数据结构:坚实的基石
在 JDK 1.8 中,ConcurrentHashMap 摒弃了之前的分段锁(Segment)设计,转而采用了与 HashMap 类似的 数组 + 链表 + 红黑树 的结构。
- 核心改进:利用
synchronized和 CAS (Compare And Swap) 来保证线程安全。 - 存储单元:使用
Node数组。当链表长度超过 8 且数组容量大于 64 时,链表会转换为红黑树以优化查询效率。
二、 putVal() 方法:高并发写入的艺术
putVal() 是整个类中最核心的逻辑。它并没有使用全局锁,而是通过细粒度的控制来实现并发。
关键步骤解读:
-
无限循环 (forndoe) :确保数据一定能插入成功。
-
检查空表:如果
table未初始化,先执行初始化。 -
CAS 插入头节点:计算 Hash 位置,如果该位置为空,直接通过 CAS 尝试放置新节点。如果成功,直接跳出。
-
检测扩容 (MOVED) :如果发现节点 Hash 值为
-1,说明正在扩容,当前线程会加入协助扩容。 -
锁定头节点 (synchronized) :如果该位置已有数据且非扩容态,则对该桶的头节点加锁。
- 若是链表,遍历寻找 Key,找到则更新,没找到则尾插。
- 若是红黑树,按照红黑树规则插入。
-
树化检查:插入完成后,判断是否需要将链表转为红黑树。
注意:
ConcurrentHashMap不允许null作为 Key 或 Value,这是为了避免在并发环境下的歧义(无法区分是“值不存在”还是“值为 null”)。
三、 get() 方法:极致的查询性能
相较于复杂的写入,get() 方法非常简洁,且全程不加锁。
关键步骤解读:
-
计算 Hash:计算 Key 的 Hash 值。
-
定位桶位置:如果头节点直接匹配,返回结果。
-
多态查找:
- 如果头节点的
hash < 0(说明是红黑树或者是正在扩容的 ForwardingNode),调用对应节点的find()方法进行查找。 - 如果是普通链表,循环遍历。
- 如果头节点的
-
返回结果:找不到则返回
null。
为什么 get() 不需要加锁?
这是因为 Node 内部的 val 和 next 指针都使用了 volatile 修饰:
这保证了当一个线程修改了节点值或插入了新节点,其他线程能立即看到最新的变化(可见性)。
四、 总结
- 锁粒度:从 JDK 1.7 的 Segment 级别优化到了 JDK 1.8 的 Node 级别,并发度更高。
- 性能:读操作完全无锁,写操作局部加锁。
- 适用场景:高并发下的键值对存储,是构建本地缓存的首选工具。