ConcurrentHashMap
ConcurrentHashMap 是高并发场景下最常用的线程安全 Map。它解决了 HashMap 并发不安全和 Hashtable 粗粒度同步性能差的问题。
# 1. 核心特点
| 特点 | 说明 |
|---|---|
| 线程安全 | 支持多线程并发访问 |
| 高并发读 | 读操作通常无锁 |
| 分桶更新 | 写操作尽量缩小锁粒度 |
| 不允许 null | key 和 value 都不能为 null |
| 弱一致性迭代 | 遍历时允许并发修改 |
| 原子复合方法 | putIfAbsent、computeIfAbsent 等 |
# 2. 为什么不允许 null
HashMap 可以:
map.put("A", null);
但 ConcurrentHashMap 不允许 null key/value。
原因之一是并发下需要清晰区分:
get(key) == null
├─ key 不存在?
└─ value 就是 null?
在并发场景中,这种歧义会影响原子判断和可见性语义。
# 3. 数据结构
Java 8 之后的结构类似:
table: Node<K,V>[]
index 0 -> Node -> Node
index 1 -> null
index 2 -> TreeBin 红黑树包装
节点字段中 value 和 next 等需要保证可见性。
读操作通过 volatile 语义读取 table 和节点值,尽量不加锁。
# 4. put 流程
简化流程:
put(key, value)
│
├─ 不允许 null,先检查
│
▼
计算 hash,定位桶
│
├─ 桶为空:CAS 放入节点
└─ 桶非空:锁住桶头节点,链表/树中插入或更新
│
▼
必要时协助扩容
关键思想:锁住的是桶级别,不是整个 Map。
线程 A 写 table[1]
线程 B 写 table[8]
可以并发推进
# 5. get 流程
get(key)
│
▼
计算 hash 定位桶
│
├─ 桶为空:返回 null
├─ 首节点匹配:返回 value
├─ 树节点:树查找
└─ 链表:遍历查找
读操作通常不加锁,因此性能很好。
# 6. 扩容
扩容时多个线程可以协助迁移。
线程 A 发现需要扩容
│
▼
创建新 table
│
▼
多个线程分段迁移桶
│
▼
迁移完成后 table 指向新数组
迁移中的桶可能放置特殊转发节点,读写线程发现后会去新表继续操作或协助迁移。
# 7. size 为什么不简单
并发环境下,精确 size 需要统计多个线程的更新。
ConcurrentHashMap 使用类似分散计数的思想降低竞争。
baseCount
CounterCell[0]
CounterCell[1]
...
sum = baseCount + cells
因此 size() 在并发修改时只是瞬时结果,不适合做强一致业务判断。
# 8. 原子方法
# 8.1 putIfAbsent
map.putIfAbsent(key, value);
原子地表达:
key 不存在 -> 放入
key 已存在 -> 不覆盖
# 8.2 computeIfAbsent
Value value = map.computeIfAbsent(key, k -> createValue(k));
常用于缓存初始化。
注意:计算函数应短小,不要在里面做长时间阻塞或递归更新同一个 Map。
# 8.3 remove(key, value)
map.remove(key, expectedValue);
只有当前 value 等于预期值时才删除。
# 9. 与 HashMap、Hashtable
| 对比项 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | 否 | 是 | 是 |
| 锁粒度 | 无 | 整表锁 | 桶级/CAS |
| null | 允许 | 不允许 | 不允许 |
| 并发性能 | 不安全 | 较差 | 好 |
生产并发 Map 优先 ConcurrentHashMap。
# 10. 常见使用场景
- 本地缓存。
- 并发计数容器。
- 去重 Set:
ConcurrentHashMap.newKeySet()。 - 多线程任务状态表。
- 连接、会话、订阅关系管理。
计数示例:
ConcurrentHashMap<String, LongAdder> counter = new ConcurrentHashMap<>();
counter.computeIfAbsent(key, k -> new LongAdder()).increment();
# 11. 注意事项
- 不要用
size()做并发强一致判断。 - 不要在
compute函数中执行耗时阻塞操作。 - 复合业务操作仍需额外同步或设计。
- 容器线程安全不代表 value 对象线程安全。
ConcurrentHashMap<String, List<String>> map = new ConcurrentHashMap<>();
map.computeIfAbsent("A", k -> new ArrayList<>()).add("x");
这里 ArrayList 本身仍然不是线程安全的。
# Tips 快问快答
Q:ConcurrentHashMap 允许 null 吗? A:不允许 null key 和 null value。
Q:ConcurrentHashMap 读需要加锁吗? A:大多数读操作不加锁,依赖 volatile 可见性等机制。
Q:Java 8 后还是分段锁吗? A:不再是老的 Segment 分段结构,主要是 CAS + 桶级同步。
Q:size 在并发下准确吗? A:只是瞬时统计,不适合做强一致业务判断。
Q:computeIfAbsent 是原子的吗? A:对同一个 key 的计算和放入具有原子语义。
Q:value 对象也线程安全吗? A:不一定。Map 结构安全不代表 value 内部安全。
Q:并发 Set 怎么实现?
A:使用 ConcurrentHashMap.newKeySet()。
Q:为什么比 Hashtable 性能好? A:锁粒度更细,读操作通常无锁,并发度更高。