Java哈希表并发演进:从HashMap到ConcurrentHashMap的线程安全机制
HashMap 的并发缺陷分析
在多线程环境中直接使用 HashMap 会引发数据丢失、结构损坏甚至死循环。其根本原因在于底层数组的索引计算与节点挂载过程缺乏任何同步控制。当多个线程同时执行插入操作且发生哈希冲突时,后执行的线程会直接覆盖先执行线程写入的键值对。此外,在扩容迁移或链表转红黑树的过程中,并发修改会导致节点引用错乱,破坏数据结构的完整性。
public class UnsafeKeyValueStore<K, V> {
private transient BucketNode<K, V>[] storage;
static class BucketNode<K, V> {
final int hashcode;
final K key;
V data;
BucketNode<K, V> link;
}
// 并发写入时缺乏同步屏障,索引冲突将直接引发数据覆盖
public void insert(K k, V v) {
int idx = computeIndex(k);
BucketNode<K, V> target = storage[idx];
if (target == null) {
storage[idx] = new BucketNode<>(k.hashCode(), k, v, null);
} else {
// 多线程同时执行此分支会导致节点覆盖或链表断裂
target.data = v;
}
}
}
Hashtable 的全局同步策略
Hashtable 采用了一种粗粒度的同步方案:直接在所有核心公开方法(如 get、put、remove)上声明 synchronized 关键字。该设计虽然严格保证了线程安全,但锁的作用域覆盖了整个哈希表实例。即使多个线程操作的是完全不同的哈希槽位,也会因为竞争同一把对象监视器锁而被迫串行执行,在高并发场景下会成为严重的性能瓶颈。
public class SynchronizedTable<K, V> {
private transient TableEntry<K, V>[] entries;
static class TableEntry<K, V> {
final int hashcode;
final K key;
V data;
TableEntry<K, V> link;
}
// 方法级同步,锁住整个实例,无关槽位的操作也会相互阻塞
public synchronized V fetch(Object k) {
// 遍历查找逻辑...
return null;
}
public synchronized V store(K k, V v) {
// 写入与冲突处理逻辑...
return null;
}
}
ConcurrentHashMap JDK 7 的分段锁架构
为突破全局锁的吞吐量限制,JDK 7 引入了分段锁(Segment)设计。底层由 Segment 数组与 HashEntry 数组构成,每个 Segment 继承自 ReentrantLock
在统计元素总数(size())时,采用乐观锁策略:先无锁累加各分段的计数值与修改版本号。若连续两次快照的修改版本号一致,则认为期间无并发写入,直接返回累加结果;若检测到版本变更,则重试一定次数后转为对全部分段加锁的悲观策略,完成精确统计。
public class SegmentedConcurrentMap<K, V> {
final LockSegment<K, V>[] partitions;
static class DataItem<K, V> {
final int hashcode;
final K key;
volatile V data;
volatile DataItem<K, V> link;
}
static class LockSegment<K, V> extends ReentrantLock {
transient volatile DataItem<K, V>[] bucket;
final V storeItem(K k, int h, V v) {
DataItem<K, V> node = tryLock() ? null : spinAcquireLock(k, h, v);
try {
// 执行分段内安全写入
return null;
} finally {
unlock();
}
}
private DataItem<K, V> spinAcquireLock(K k, int h, V v) {
while (!tryLock()) {
// 自旋等待锁释放,避免立即阻塞
}
return null;
}
}
public V put(K k, V v) {
if (v == null) throw new NullPointerException();
// 定位目标分段并委托给 storeItem 处理
return null;
}
}
ConcurrentHashMap JDK 8 的细粒度锁与分散计数
JDK 8 彻底废弃了 Segment 架构,回归到与 HashMap 相似的 Node 数组结构,但同步机制更为精细。插入或更新数据时,仅对目标桶(Bucket)的头节点使用 synchronized 加锁。由于链表遍历与红黑树操作均始于头节点,锁定头节点即可安全控制该槽位的所有变更,临界区大幅缩小,锁竞争概率显著降低。
元素计数方面,摒弃了全局累加模式,转而采用 baseCount 结合 CounterCell 数组的分散计数方案。无竞争时直接通过 CAS 更新基础计数器;发生线程竞争时,将增量分散写入 CounterCell
public class ModernConcurrentMap<K, V> {
transient volatile BucketNode<K, V>[] table;
private transient volatile long baseCounter;
private transient volatile CountCell[] countCells;
static class BucketNode<K, V> {
final int hashcode;
final K key;
volatile V val;
volatile BucketNode<K, V> next;
}
final V insertValue(K k, V v) {
if (k == null || v == null) throw new NullPointerException();
for (BucketNode<K, V>[] tab = table;;) {
BucketNode<K, V> head = tabAt(tab, index);
if (head == null) {
// CAS 尝试初始化空桶
} else {
// 仅锁定当前桶的头节点,极大缩小同步范围
synchronized (head) {
// 执行链表尾插或红黑树平衡操作
}
}
updateCount(1L);
return null;
}
}
private void updateCount(long delta) {
// 优先 CAS 更新 baseCounter
// 竞争激烈时将增量分散写入 countCells 数组单元
}
}