REJAVA / LEARNING NOTES
第6课速查:ConcurrentHashMap(并发哈希映射) 七问
随时回查术语、规则和示例;完整解释见对应课程。
根据课程材料整理
本次更新说明
- 页尾去掉本地资料入口。
- 按新版第6课改为七个要点。
- 整理为公开学习笔记,保留技术内容与练习。
- 清理个人分析,完善阅读导航。
1 · 线程安全,为什么还少算?
它只保证 单次方法调用 原子。get(读取方法) 再 put(写入方法) 是两次调用:两线程读到同一旧值,后写覆盖先写 → lost update(更新丢失)。和 i++ 同类问题。
2 · 怎么改?
计数:merge(k, 1, Integer::sum),返回新值。
putIfAbsent(不存在才放入的方法):值先造好,返回旧值或 null(空值)。
computeIfAbsent(缺失时计算的方法):不存在才执行函数,返回现值。
compute(原子重算方法):按当前值(可能 null(空值))算新值。
函数要短,不调远程接口,不改本表;函数返回 null(空值):computeIfAbsent(缺失时计算的方法) 不放入,compute(原子重算方法)/merge(原子合并方法) 删除该 key(键)。
3 · 还要自己管什么?
value(值) 是 ArrayList(可变数组列表) 时的 add(追加方法);跨两个 key(键) 一起生效;先查 size(数量方法) 再插入;遍历不是快照(弱一致,不抛 ConcurrentModificationException(并发修改异常))。
put(写入方法) 之前的写,对 get(读取方法) 到该值的线程可见;入表后再改字段不保证。
4 · JDK 7 vs JDK 8
7: Segment(分段锁) 分段,每段一把 ReentrantLock(可重入锁),默认 16 段。
8+: Node 数组 + 链表/红黑树;空桶 CAS(比较并交换);非空桶 synchronized(内置同步关键字) 锁桶头;get(读取方法) 不加锁。
用 synchronized(内置同步关键字):首节点直接当锁,省去每桶一个锁对象。
5 · put(写入方法) 流程
null(空值) 检查 → spread 算 hash(哈希) → 循环:没表先建表/空桶 CAS(比较并交换)/遇转发节点先帮忙扩容/否则锁桶头,链表或树里替换或追加 → 链表 ≥ 8 时 treeifyBin(数组 < 64 先扩容)→ addCount 计数并检查扩容。
扩容:多线程分段协助迁移,迁完的旧桶放 forwarding node(转发节点)。
6 · 为什么不许 null(空值)
避免歧义:get(读取方法) 返回 null(空值) 分不清“没有”还是“值为 null(空值)”;并发下无法再用 containsKey 补判。禁止后,null(空值) 只表示没有。
7 · size(数量方法) 准不准
baseCount + CounterCell(计数单元) 数组(LongAdder(分散累加器) 思路):无竞争 CAS(比较并交换) 改 baseCount,有竞争分散到格子。size(数量方法) 求和,并发下是估计值;推荐 mappingCount(返回 long)。