HashMap的线程安全问题

Map<String, String> map = new HashMap<>();

// 多线程并发put
map.put("key1", "value1");  // 线程1
map.put("key2", "value2");  // 线程2

// 问题1:数据丢失
// 问题2:死循环(JDK 7)
// 问题3:数据覆盖

三种解决方案

// 方案1:Hashtable(废弃)
Map<String, String> map = new Hashtable<>();  // synchronized,性能差

// 方案2:Collections.synchronizedMap
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());  // 性能差

// 方案3:ConcurrentHashMap(推荐)
Map<String, String> map = new ConcurrentHashMap<>();  // 高性能

JDK 7实现:分段锁

核心思想:将Map分成多个Segment,每个Segment独立加锁

ConcurrentHashMap
├─ Segment[0]  (ReentrantLock)
│  ├─ HashEntry[0]
│  ├─ HashEntry[1]
│  └─ ...
├─ Segment[1]  (ReentrantLock)
│  ├─ HashEntry[0]
│  └─ ...
└─ ...

优点:降低锁粒度,提高并发度 缺点

  • 内存占用大
  • 扩容复杂
  • 最多并发度 = Segment数量(默认16)

JDK 8实现:CAS + synchronized

核心变化

  1. 取消Segment,直接用数组
  2. 使用CAS + synchronized
  3. 链表长度 > 8 转为红黑树

关键源码(简化)

public V put(K key, V value) {
    int hash = spread(key.hashCode());

    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f;
        int n, i, fh;

        if (tab == null || (n = tab.length) == 0)
            tab = initTable();  // 初始化

        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 桶为空,CAS插入
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break;
        }

        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);  // 协助扩容

        else {
            // 桶不为空,synchronized插入
            synchronized (f) {
                // 链表或红黑树插入
            }
        }
    }

    addCount(1L, binCount);
    return null;
}

核心机制

1. CAS无锁插入

// 桶为空时,用CAS插入(无锁)
if (casTabAt(tab, i, null, newNode))
    break;  // 插入成功

2. synchronized锁桶

// 桶不为空,锁住头节点
synchronized (头节点) {
    // 链表/红黑树插入
}

优点

  • 只锁一个桶,并发度高
  • JDK 6后synchronized性能优化

3. 红黑树优化

// 链表长度 > 8,转红黑树
if (binCount >= TREEIFY_THRESHOLD)
    treeifyBin(tab, i);

优点:O(n) → O(log n)

4. 扩容优化

// 多线程协助扩容
if (当前正在扩容) {
    helpTransfer();  // 协助迁移数据
}

性能对比

实现读性能写性能并发度内存
Hashtable1
synchronizedMap1
CHM JDK 716
CHM JDK 8更好更好无限

测试代码

// 10线程,各10万次put
Hashtable:         5800ms
synchronizedMap:   5200ms
ConcurrentHashMap: 450ms

重要方法

1. putIfAbsent

// 原子操作:不存在时才put
V oldValue = map.putIfAbsent("key", "value");
if (oldValue == null) {
    // 插入成功
} else {
    // key已存在
}

2. computeIfAbsent

// 原子操作:不存在时计算并put
String value = map.computeIfAbsent("key", k -> {
    return expensiveComputation(k);  // 只计算一次
});

3. merge

// 原子操作:合并值
map.merge("key", 1, (oldValue, newValue) -> oldValue + newValue);
// 等价于:map.put("key", map.getOrDefault("key", 0) + 1);

最佳实践

1. 初始容量设置

// 预估大小1000,负载因子0.75
int initialCapacity = (int) (1000 / 0.75) + 1;
Map<String, String> map = new ConcurrentHashMap<>(initialCapacity);

2. 避免全表锁操作

// ❌ size()在JDK 7会锁所有Segment
int size = map.size();

// ✅ 使用mappingCount()
long size = map.mappingCount();  // 不加锁,返回近似值

3. 批量操作

// ✅ 使用parallelStream
map.entrySet().parallelStream()
   .forEach(entry -> process(entry));

常见问题

Q1:ConcurrentHashMap允许null吗?

// ❌ key和value都不允许null
map.put(null, "value");  // NullPointerException
map.put("key", null);    // NullPointerException

Q2:size()准确吗?

// JDK 8:size()和mappingCount()都是近似值
// 不保证实时准确性

Q3:迭代时可以修改吗?

// ✅ 弱一致性,不抛ConcurrentModificationException
for (String key : map.keySet()) {
    map.put(key + "_new", "value");  // 允许
}

总结

核心要点

  1. JDK 7:分段锁,最多并发16
  2. JDK 8:CAS + synchronized,无限并发
  3. 性能:远超Hashtable和synchronizedMap

使用建议

  • 高并发场景首选
  • 不允许null
  • size()是近似值
  • 使用computeIfAbsent等原子方法

系列文章