concurrenthashmap线程安全靠细粒度锁:jdk1.7用segment分段锁,jdk1.8改用synchronized锁单桶+cas;get无锁依赖volatile和数组引用不可变性,put通过cas及桶级锁避免覆盖。

ConcurrentHashMap 的线程安全不是靠 synchronized 锁整个 map
它用的是更细粒度的控制:JDK 1.7 中是 Segment 分段锁,JDK 1.8 彻底废弃了 Segment,改用 Node 数组 + synchronized 锁单个桶(bin)+ CAS 操作。这意味着多个线程只要不操作同一个哈希桶,就能完全并发执行。
常见误解是“它内部用了 ReentrantLock”,其实 JDK 1.8 中写操作(如 put、remove)只对目标桶头节点加 synchronized,读操作(如 get)甚至完全无锁——依赖 volatile 语义和数组引用的不可变性保证可见性。
为什么 get 不加锁也能线程安全
get 方法不加锁,但依然能读到最新值,关键在三点:
-
table数组声明为transient volatile Node<k>[] table</k>,确保数组引用更新对所有线程可见 - 每个
Node的val和next字段都是volatile,保证链表/红黑树结构变更的可见性 - 扩容时新老数组并存,
get会先查新表,查不到再查旧表,且迁移过程保证节点不会丢失或重复
注意:get 不保证实时一致性——它可能读到“正在迁移中”的中间状态(比如某个桶刚被迁走但旧引用还没置空),但绝不会读到断裂链表或空指针异常。
立即学习“Java免费学习笔记(深入)”;
put 过程中如何避免并发覆盖
put 的核心逻辑是:定位桶 → 若为空,尝试 CAS 插入;若非空,加锁后遍历链表/树插入。关键点在于:
- 初始插入用
U.compareAndSwapObject(tab, ((long)i ,失败则重试或加锁 - 链表转红黑树阈值是
8,但前提是桶数组长度 ≥64,否则先扩容而不是树化 - 多个线程同时触发扩容时,会协作迁移,通过
sizeCtl控制扩容线程数,避免重复初始化新表
Node<K,V>[] tab; int n;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = initTable()).length;
int hash = spread(key.hashCode());
int i = (n - 1) & hash;
Node<K,V> f = tabAt(tab, i);
if (f == null) {
if (casTabAt(tab, i, null, new Node<>(hash, key, value, null)))
break;
}别把 ConcurrentHashMap 当作“线程安全的 HashMap”来用
它只保证单个操作(put、get、remove)原子,不保证复合操作的线程安全。比如:
-
map.get(key) == null && map.put(key, value)是竞态的,应改用map.putIfAbsent(key, value) -
map.size()返回的是估算值(可能滞后),高并发下不准;需要精确计数建议用LongAdder配合 -
computeIfAbsent内部会加锁并阻塞其他线程访问该桶,若传入的 mappingFunction 耗时长,会成为性能瓶颈
最常被忽略的一点:迭代器(keySet().iterator())是弱一致性的——它不抛 ConcurrentModificationException,但也不保证反映某一时刻的快照;遍历时可能跳过新插入元素,也可能重复读取已删除节点(取决于迁移时机)。










