Skip to content

ConcurrentHashMap

HashMap 是 Java 中最常用的数据结构,但它是线程不安全的。

多线程环境下用 HashMap?轻则数据错乱,重则死循环 CPU 100%。ConcurrentHashMap 就是为并发场景设计的 HashMap。

为什么 HashMap 不是线程安全的

问题一:并发 put 导致数据覆盖

java
// 两个线程同时 put("key", value),只有一个能成功
map.put("key", valueA);
map.put("key", valueB);
// 期望:valueB
// 实际:可能还是 valueA

问题二:扩容时死循环(JDK 7)

JDK 7 的 HashMap 在并发扩容时,多线程 rehash 可能导致链表成环,形成死循环:

线程A: 扩容中,rehash 链表
线程B: 同时访问,遍历到成环的链表
结果: while(true) 死循环,CPU 100%

JDK 8 虽然修复了死循环问题,但并发 put 仍然不安全。

ConcurrentHashMap 的演进

版本实现特点
JDK 7Segment + 数组 + 链表锁分段,16 个 Segment
JDK 8+CAS + synchronized + 红黑树锁细化,只锁单个桶

JDK 8 是重大升级,去掉了 Segment,直接用 CAS + synchronized:

┌──────────────────────────────────────────────────┐
│            JDK 8 ConcurrentHashMap 结构            │
├──────────────────────────────────────────────────┤
│                                                  │
│  数组(桶)                                        │
│    │                                             │
│    ├── 链表(JDK 8: 长度 > 8 时转为红黑树)         │
│    │                                             │
│    └── 红黑树(JDK 8+)                            │
│        └── 节点数 < 6 时退化为链表                  │
│                                                  │
│  并发控制:                                        │
│  - put: CAS + synchronized(只锁当前桶)           │
│  - get: 无锁(volatile 保证可见性)                │
│                                                  │
└──────────────────────────────────────────────────┘

基础用法

java
public class ConcurrentHashMapDemo {

    private static final ConcurrentHashMap&lt;String, Integer&gt; map =
        new ConcurrentHashMap&lt;&gt;();

    public static void main(String[] args) {
        // put
        map.put("A", 1);
        map.put("B", 2);

        // get(无锁)
        System.out.println("A: " + map.get("A"));

        // putIfAbsent(原子操作)
        map.putIfAbsent("C", 3);
        map.putIfAbsent("A", 100);  // 不会覆盖,因为 A 已存在

        // remove(原子操作)
        map.remove("A");

        // replace(原子操作)
        map.put("B", 2);
        map.replace("B", 2, 3);  // 条件替换

        System.out.println("Map: " + map);
    }
}

原子操作

ConcurrentHashMap 提供了多个原子操作:

java
public class AtomicOperationsDemo {

    private static final ConcurrentHashMap&lt;String, AtomicInteger&gt; map =
        new ConcurrentHashMap&lt;&gt;();

    public static void main(String[] args) {
        // computeIfAbsent:原子初始化
        AtomicInteger count = map.computeIfAbsent("key",
            k -&gt; new AtomicInteger());

        // 原子累加
        map.computeIfAbsent("requests",
            k -&gt; new AtomicInteger()).incrementAndGet();
        map.computeIfAbsent("requests",
            k -&gt; new AtomicInteger()).incrementAndGet();

        System.out.println("计数: " + map.get("requests").get());
    }
}

高性能计数:LongAdder 组合

java
public class AggregationDemo {

    private static final ConcurrentHashMap&lt;String, LongAdder&gt; counter =
        new ConcurrentHashMap&lt;&gt;();

    public static void main(String[] args) {
        // 高性能计数:多个线程同时累加
        for (int i = 0; i &lt; 1000; i++) {
            counter.computeIfAbsent("pageA", k -&gt; new LongAdder()).increment();
        }

        for (int i = 0; i &lt; 500; i++) {
            counter.computeIfAbsent("pageB", k -&gt; new LongAdder()).increment();
        }

        counter.forEach((k, v) -&gt;
            System.out.println(k + ": " + v.sum()));
    }
}

这种模式在高并发统计场景下性能极高,因为 LongAdder 内部是分段累加,减少了竞争。

JDK 8 新增方法

java
public class Jdk8MethodsDemo {

    private static final ConcurrentHashMap&lt;String, Integer&gt; map =
        new ConcurrentHashMap&lt;&gt;();

    public static void main(String[] args) {
        map.put("A", 1);
        map.put("B", 2);

        // forEach:遍历
        map.forEach((k, v) -&gt; System.out.println(k + "=" + v));

        // search:并行搜索
        String result = map.search(1, (k, v) -&gt; v &gt; 1 ? k : null);
        System.out.println("第一个值&gt;1的键: " + result);

        // reduce:并行聚合
        Integer sum = map.reduce(1, (k, v) -&gt; v, Integer::sum);
        System.out.println("值的总和: " + sum);

        // merge:原子合并
        map.merge("C", 10, (old, newVal) -&gt; old + newVal);
        System.out.println("合并后 C: " + map.get("C"));
    }
}

ConcurrentHashMap vs 其他 Map

特性ConcurrentHashMapHashtableCollections.synchronizedMap
并发度高(桶级锁)低(全局锁)低(全局锁)
null 支持key/value 都不允许不允许允许
迭代器弱一致性快速失败快速失败
get() 性能无锁,高加锁加锁

弱一致性迭代器

ConcurrentHashMap 的迭代器不会抛出 ConcurrentModificationException,而是弱一致的:

java
public class IteratorDemo {

    private static final ConcurrentHashMap&lt;String, Integer&gt; map =
        new ConcurrentHashMap&lt;&gt;();

    public static void main(String[] args) {
        map.put("A", 1);
        map.put("B", 2);

        // 迭代过程中,其他线程的修改可能看到或看不到
        for (Map.Entry&lt;String, Integer&gt; entry : map.entrySet()) {
            System.out.println(entry.getKey() + "=" + entry.getValue());
        }

        // size() 可能不准确
        System.out.println("size: " + map.size());
    }
}

常见错误

错误一:复合操作非原子

java
// ❌ 错误:get + compute 不是原子操作
Integer old = map.get("key");
Integer newVal = (old == null) ? 1 : old + 1;
map.put("key", newVal);

// ✅ 正确:使用原子方法
map.compute("key", (k, v) -&gt; v == null ? 1 : v + 1);

// 或使用 putIfAbsent 组合

错误二:key/value 用了 null

java
// ❌ 错误
map.put(null, 1);      // 不允许 null key
map.put("key", null);  // 不允许 null value

// ✅ 正确:使用空对象或 Optional
map.put("key", 0);     // 用 0 表示不存在

性能调优

初始容量

java
// 根据预估数据量设置,避免频繁扩容
ConcurrentHashMap&lt;String, Integer&gt; map =
    new ConcurrentHashMap&lt;&gt;(1000);  // 预估 1000 个元素

并发度设置

java
// JDK 8 默认并发度是 16
// 可以根据线程数调整
ConcurrentHashMap&lt;String, Object&gt; map =
    new ConcurrentHashMap&lt;&gt;(16, 0.75f, 32);  // 初始16,加载因子0.75,并发度32

要点回顾

  • ConcurrentHashMap 是线程安全的高性能 Map
  • JDK 7 用 Segment 锁分段,JDK 8 用 CAS + synchronized 锁细化
  • get() 无锁,put() 只锁当前桶
  • 提供了丰富的原子操作:computeIfAbsent、merge、replace 等
  • key/value 不允许为 null
  • 迭代器是弱一致的,不抛 ConcurrentModificationException

相关链接

基于 VitePress 构建