Java 并发容器深度解析:HashMap 线程安全问题与高性能线程安全 Map 实现

270 阅读15分钟

作为 Java 开发者,HashMap 简直就是家常便饭。但在多线程环境下,各种诡异的问题就会找上门来。有一次我们的交易系统 CPU 突然飙到 100%,服务直接挂了。排查下来一看,一堆线程全卡在 HashMap.get()上死循环。这次教训让我彻底研究了 HashMap 的线程安全性问题,总结出这篇实战经验,希望能帮你避坑。

HashMap 为什么不是线程安全的?

先来看 HashMap 在 Java 8 中的内部结构:一个 Node<K,V>[]数组,哈希冲突时通过链表或红黑树解决。

graph TD
    A[HashMap] --> B[Node数组]
    B --> C1[bucket 0]
    B --> C2[bucket 1]
    B --> C3[bucket ...]
    B --> C4[bucket n-1]
    C2 --> D1[Node]
    D1 --> D2[Node]
    D2 --> D3[Node ...]

往 HashMap 中放一个键值对时,大致执行这些步骤:

  1. 计算 key 的哈希值
  2. 根据哈希值定位到数组位置
  3. 如果该位置为空,直接放入新 Node
  4. 如果该位置已有元素,则遍历链表/树,找到相同 key 就更新值,否则添加新 Node
  5. 检查是否需要扩容

单线程下一切正常,但多线程环境下这个过程不是原子的,会导致各种奇葩问题:

1. 数据丢失问题

看个例子就明白了,多线程并发修改 HashMap 时到底会发生什么:

public class HashMapLostUpdateDemo {
    public static void main(String[] args) throws Exception {
        final Map<String, String> map = new HashMap<>();

        // 两个线程更新相同位置的数据
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                map.put("key" + i % 8, "value from t1-" + i);
                try { Thread.sleep(1); } catch (Exception e) {}
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                map.put("key" + i % 8, "value from t2-" + i);
                try { Thread.sleep(1); } catch (Exception e) {}
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("预期元素个数: 8, 实际元素个数: " + map.size());
        // 检查是否有元素丢失
        for (int i = 0; i < 8; i++) {
            if (map.get("key" + i) == null) {
                System.out.println("key" + i + " 丢失了!");
            }
        }
    }
}

多跑几次这段代码,你会发现实际元素个数可能小于 8,说明有元素丢失了。问题不仅仅是值覆盖,更要命的是并发修改导致的结构破坏。当多个线程同时修改 HashMap 时,尤其是触发扩容的时候,链表结构可能会变得混乱,导致某些节点"凭空消失"。

2. 死循环

在 Java 7 中,HashMap 扩容时使用头插法重建链表。多线程环境下,这可能导致链表形成环形结构:

这个问题在生产环境特别难查。我们的事故就是这样,系统运行好好的,突然 CPU 飙升到 100%。通过jstack -l <pid>发现大量线程卡在 HashMap.get()方法上。分析堆转储(用jmap -dump:live,format=b,file=heap.bin <pid>获取)后发现链表中存在环形引用。

Java 8 改为尾插法后,彻底解决了环形链表死循环问题,但并发修改仍可能导致元素丢失。

3. 并发修改异常

最常见的问题是 ConcurrentModificationException,当一个线程遍历 HashMap 时,另一个线程修改了结构:

public class ConcurrentModificationDemo {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        map.put("a", "1");
        map.put("b", "2");

        Thread t1 = new Thread(() -> {
            try {
                for (Map.Entry<String, String> entry : map.entrySet()) {
                    Thread.sleep(100); // 模拟慢操作
                    System.out.println(entry.getKey() + ":" + entry.getValue());
                }
            } catch (Exception e) {
                System.out.println("线程t1异常: " + e.getClass().getName());
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(50); // 确保在遍历中修改
                map.put("c", "3");
                System.out.println("线程t2添加了新元素c=3");
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        t1.start();
        t2.start();
    }
}

这段代码几乎 100%会抛出 ConcurrentModificationException。原因是 HashMap 内部维护了一个 modCount 计数器记录结构修改次数。创建迭代器时会保存当前的 modCount,遍历过程中检查到 modCount 变化就抛异常。

这种设计叫"fail-fast"(快速失败)——发现并发修改立即报错,而不是继续处理可能已经不一致的数据。就像你往前走路时,突然发现前面有坑,立马停下来比冒然踩下去要好得多。

HashMap 线程安全问题的本质

归根结底,HashMap 线程不安全的原因是:

  • 非原子操作:插入、删除、扩容等操作包含多个步骤,没有事务保护
  • 可见性问题:HashMap 的字段没有 volatile 修饰,线程间修改不可见
  • 结构共享:多线程同时修改共享的数组和链表,导致结构混乱

打个比方,HashMap 就像一个没有服务员的自助餐厅,每个人都可以随时往任何位置放食物或拿走食物。没有协调的情况下,必然会出现多人同时操作同一个位置,导致食物被覆盖、丢失,或者餐桌结构被破坏。

Java 中的线程安全 Map 实现

好在 Java 提供了几种线程安全的 Map 实现,我们先看看这些现成的解决方案:

1. Collections.synchronizedMap

最简单直接的方法,给 HashMap 套上 synchronized:

Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());

实现原理就是所有操作都要先获取锁:

public V get(Object key) {
    synchronized (mutex) {
        return m.get(key);
    }
}

public V put(K key, V value) {
    synchronized (mutex) {
        return m.put(key, value);
    }
}

这种方案有个容易忽略的坑:遍历时也需要手动同步,否则还是会抛异常:

Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
// 必须获取锁后才能安全遍历
synchronized (syncMap) { // 获取的是mutex对象锁
    for (Map.Entry<String, String> entry : syncMap.entrySet()) {
        // 处理entry
    }
}

2. ConcurrentHashMap

这是专为并发设计的高性能 Map,在 Java 8 中的实现非常精妙:

graph TD
    A[ConcurrentHashMap] --> B[volatile Node数组]
    B --> C1[bucket 0]
    B --> C2[bucket 1]
    B --> C3[bucket ...]
    B --> C4[bucket n-1]
    C2 -->|"synchronized(首节点)"| D1[Node]
    D1 --> D2[Node]
    D2 --> D3[Node ...]

    style C2 fill:#f9f,stroke:#333
    style D1 fill:#ff9,stroke:#333

ConcurrentHashMap 的并发控制有几个关键点:

  1. 细粒度锁:只锁定当前操作的链表首节点,不同的哈希桶可以并发操作
  2. 读操作完全无锁:get 方法无需加锁,通过 volatile 保证可见性
  3. volatile 保证可见性:Node.val 和 Node.next 是 volatile 的,保证读操作能看到最新值(Node.key 不需要 volatile 因为 key 一旦设置就不会变)
  4. CAS + synchronized 结合:先尝试 CAS 无锁更新,失败再用 synchronized
  5. 分段迁移扩容:通过 sizeCtl 和 transferIndex 等状态变量,多线程协作完成扩容

例如 put 操作的简化逻辑:

1. 如果桶为空,用CAS设置新节点(无锁)
2. 如果CAS失败或桶不为空,对首节点加synchronized锁
3. 在锁内完成链表/红黑树的更新

ConcurrentHashMap 的扩容是个精妙的设计,通过特殊的 ForwardingNode 标记节点,允许多个线程同时参与扩容过程:

// 简化的扩容状态控制
private transient volatile int sizeCtl;
// sizeCtl < 0 表示有线程在进行初始化或扩容
// sizeCtl = -1 表示正在初始化
// sizeCtl = -N 表示有N-1个线程在进行扩容

特别之处在于,ConcurrentHashMap 的迭代器是"fail-safe"的,不会抛 ConcurrentModificationException。它基于弱一致性设计——遍历过程中可以继续修改 Map,但可能看不到最新修改。就像你拍了一张照片,照片里的东西不会变,但真实世界已经变了。

3. Hashtable

最古老的线程安全 Map,但现在基本没人用了:

Map<String, String> table = new Hashtable<>();

它给所有方法都加上 synchronized,导致性能奇差。就像一家餐厅只有一个服务员,每次只能服务一位客人,其他人都要排队。

HashMap 和 ConcurrentHashMap 的版本演进

HashMap 和 ConcurrentHashMap 在不同 Java 版本中的实现差异很大:

graph TD
    A[Java 7 HashMap] --> B["数组 + 链表"]
    B --> B1["头插法扩容(可能导致环形链表)"]

    C[Java 8+ HashMap] --> D["数组 + 链表/红黑树"]
    D --> D1["链表长度>8转红黑树尾插法扩容(解决环形链表)"]
graph TD
    A[Java 7 ConcurrentHashMap] --> B["Segment数组(16个分段锁)继承ReentrantLock"]
    B --> B1["每个Segment一把锁分离读写锁"]

    C[Java 8+ ConcurrentHashMap] --> D["去除Segment数组 + 链表/红黑树"]
    D --> D1["CAS + synchronized桶级别的锁"]
    D --> D2["多线程协作扩容sizeCtl状态控制"]

Java 8 后两者的内部结构更加相似,最大区别是 ConcurrentHashMap 增加了并发控制机制。

手写一个线程安全 HashMap(基于锁)

如果想自己实现线程安全的 HashMap,最直接的方式是使用读写锁:

public class ThreadSafeHashMap<K, V> {
    private final Map<K, V> map = new HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    public V get(K key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public V put(K key, V value) {
        writeLock.lock();
        try {
            return map.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    public V remove(K key) {
        writeLock.lock();
        try {
            return map.remove(key);
        } finally {
            writeLock.unlock();
        }
    }

    public boolean containsKey(K key) {
        readLock.lock();
        try {
            return map.containsKey(key);
        } finally {
            readLock.unlock();
        }
    }

    public Set<Map.Entry<K, V>> entrySet() {
        readLock.lock();
        try {
            // 返回不可修改的副本,保证不会被外部修改
            // 注意:这只提供获取时的一致性视图,不保证后续不变
            return Collections.unmodifiableSet(new HashSet<>(map.entrySet()));
        } finally {
            readLock.unlock();
        }
    }

    // 原子性组合操作示例
    public V putIfAbsent(K key, V value) {
        writeLock.lock();
        try {
            V old = map.get(key);
            if (old == null) {
                map.put(key, value);
                return null;
            }
            return old;
        } finally {
            writeLock.unlock();
        }
    }
}

读写锁允许多线程同时读取,只要没有写操作。这就像图书馆:多人可以同时阅读,但有人要整理书架时必须暂停阅读。

graph TD
    A[请求读锁] --> B{有写锁?}
    B -->|是| C[等待]
    B -->|否| D[获取读锁]
    E[请求写锁] --> F{有读锁或写锁?}
    F -->|是| G[等待]
    F -->|否| H[获取写锁]

这种实现提供了强一致性视图——在锁保护期间,你看到的数据就是最新的数据,没有中间状态。而 ConcurrentHashMap 只提供最终一致性,不保证跨方法调用的原子性。

读写锁实现有个重要优势:可以实现原子性的组合操作。比如"先检查 key 是否存在,不存在则添加"这种复合操作,在 ConcurrentHashMap 中需要用 computeIfAbsent,而读写锁实现可以自由组合任意操作。

但这种方案也有局限性:

  • 写操作完全互斥,不同 key 的写操作也会阻塞
  • 锁的粒度较大,并发性能有限
  • 读写锁本身有一定开销

无锁/低锁并发 HashMap 实现

如果追求更高性能,可以借鉴 ConcurrentHashMap 的思路,使用 CAS 和细粒度锁:

public class LockFreeHashMap<K, V> {
    private static final AtomicReferenceFieldUpdater<LockFreeHashMap, Node[]> UPDATER =
            AtomicReferenceFieldUpdater.newUpdater(LockFreeHashMap.class, Node[].class, "table");

    private static final int DEFAULT_CAPACITY = 16;
    private static final float LOAD_FACTOR = 0.75f;

    // volatile保证可见性
    private volatile Node<K, V>[] table;
    private final AtomicInteger size = new AtomicInteger(0);
    private int threshold;

    @SuppressWarnings("unchecked")
    public LockFreeHashMap(int initialCapacity) {
        table = (Node<K, V>[]) new Node[initialCapacity];
        threshold = (int) (initialCapacity * LOAD_FACTOR);
    }

    public LockFreeHashMap() {
        this(DEFAULT_CAPACITY);
    }

    private static final class Node<K, V> {
        final int hash;
        final K key;  // key无需volatile因一旦赋值不再修改
        volatile V value;  // value需要volatile保证可见性
        volatile Node<K, V> next;  // next指针需要volatile避免指令重排导致的可见性问题

        Node(int hash, K key, V value, Node<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

    // 读操作无锁
    public V get(K key) {
        int hash = hash(key);
        Node<K, V>[] tab = table;
        int n = tab.length;
        int index = (n - 1) & hash;

        for (Node<K, V> e = tab[index]; e != null; e = e.next) {
            if (e.hash == hash && (e.key == key || (key != null && key.equals(e.key)))) {
                return e.value;
            }
        }
        return null;
    }

    // 写操作使用CAS
    public V put(K key, V value) {
        int hash = hash(key);
        Node<K, V>[] tab = table;
        int n = tab.length;
        int index = (n - 1) & hash;

        // 如果桶为空,使用CAS操作创建新节点
        if (tab[index] == null) {
            Node<K, V> newNode = new Node<>(hash, key, value, null);
            if (casTabAt(index, null, newNode)) {
                size.incrementAndGet();
                if (size.get() > threshold) {
                    resize(); // 需要扩容
                }
                return null;
            }
        }

        // 如果桶不为空,遍历链表
        Node<K, V> first = tab[index];
        for (Node<K, V> e = first; e != null; e = e.next) {
            if (e.hash == hash && (e.key == key || (key != null && key.equals(e.key)))) {
                // 找到相同的key,更新value
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }

        // 没有找到相同的key,添加新节点到链表头部
        Node<K, V> newNode = new Node<>(hash, key, value, first);
        if (casTabAt(index, first, newNode)) {
            size.incrementAndGet();
            if (size.get() > threshold) {
                resize(); // 需要扩容
            }
            return null;
        }

        // CAS失败,重试
        return put(key, value);
    }

    private int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    @SuppressWarnings("unchecked")
    private boolean casTabAt(int i, Node<K, V> expect, Node<K, V> update) {
        return UPDATER.compareAndSet(this, table,
            replaceElement(table, i, expect, update));
    }

    @SuppressWarnings("unchecked")
    private Node<K, V>[] replaceElement(Node<K, V>[] arr, int i,
                                      Node<K, V> expect, Node<K, V> update) {
        Node<K, V>[] copy = Arrays.copyOf(arr, arr.length);
        if (copy[i] == expect) {
            copy[i] = update;
        }
        return copy;
    }

    // 注意:这只是简化的伪代码,真实无锁扩容极其复杂
    // 真实无锁扩容需要解决:
    // 1. 多线程协作迁移元素(通过CAS分配任务)
    // 2. 使用特殊标记节点(如ForwardingNode)标记已迁移的桶
    // 3. 处理并发读写与扩容的竞争(读操作可能需要帮助扩容)
    // 4. 解决ABA问题(通过版本号或标记位)
    private void resize() {
        /*
        无锁扩容的核心思路:
        1. 用CAS设置状态标记(类似ConcurrentHashMap的sizeCtl)
        2. 多线程协作分段迁移元素,每个线程负责一段区间
        3. 用特殊的ForwardingNode标记已迁移的桶
        4. 读操作遇到ForwardingNode时转向新表
        5. 所有桶迁移完成后切换到新table引用
        */

        // 此处省略具体实现,可参考ConcurrentHashMap源码中的transfer方法
        // 真实实现约200行代码,涉及复杂的并发控制
    }
}

这里的核心是 CAS(Compare-And-Swap)操作,它是 CPU 硬件支持的原子指令,无需加锁就能保证操作的原子性。CAS 逻辑类似:

当前值 = 读取内存位置X
如果(当前值 == 预期值)
    将新值写入X
    返回成功
否则
    返回失败

形象地说,CAS 就像你和售货员交易:

  1. 你看到一件标价 100 元的商品
  2. 你拿出 100 元递给售货员
  3. 如果价格还是 100 元,交易成功;如果价格已经变成了 120 元(别人修改了),交易失败
sequenceDiagram
    participant 线程
    participant 内存

    线程->>内存: 读取当前值A
    线程->>线程: 计算新值B
    线程->>内存: CAS(期望值A,新值B)
    alt 当前值仍为A
        内存-->>线程: 更新成功
    else 当前值已变为C
        内存-->>线程: 更新失败,需重试
    end

无锁设计在低竞争场景下性能出色,但高竞争时可能因 CAS 自旋导致性能下降。这就是为什么 ConcurrentHashMap 采用 CAS 和 synchronized 结合的方式,而不是纯 CAS 实现。

另一种完全不同的思路是使用不可变数据结构,每次修改都创建新 Map,如 Guava 的 ImmutableMap:

// 不可变Map,线程安全但修改开销大
Map<String, String> map1 = ImmutableMap.of("key1", "value1", "key2", "value2");
// 需要修改时创建新Map而非修改原Map
Map<String, String> map2 = ImmutableMap.<String, String>builder()
    .putAll(map1)
    .put("key3", "value3")
    .build();

这种方式像 Git 提交,每次修改都创建一个新版本,而不修改原版本。它在读多写极少的场景下有奇效,因为读操作完全无同步开销。

各种实现的性能对比与适用场景

不同线程安全 Map 在不同场景下的性能表现:

graph LR
    A[性能比较] --> B[读多写少]
    A --> C[读写均衡]
    A --> D[写多读少]
    A --> E[竞争程度]

    B --> B1["ConcurrentHashMap ≈ 不可变Map > 读写锁 > synchronizedMap"]
    C --> C1["低竞争: 无锁实现 ≈ ConcurrentHashMap > 读写锁 > synchronizedMap"]
    C --> C2["高竞争: ConcurrentHashMap > 读写锁 > 无锁实现 > synchronizedMap"]
    D --> D1["低竞争: 无锁实现 > ConcurrentHashMap > 读写锁 > synchronizedMap"]
    D --> D2["高竞争: ConcurrentHashMap > 读写锁 > 无锁实现 > synchronizedMap"]
    E --> E1["低竞争(线程操作不同数据): 无锁/CAS方案更优"]
    E --> E2["高竞争(线程操作相同数据): 细粒度锁方案更稳定"]

影响性能的主要因素:

  • 竞争程度:多线程操作同一数据时,CAS 会频繁自旋重试,性能下降
  • 数据规模:数据量大时,细粒度锁优势明显
  • 读写比例:读多写少场景下,读写锁和 ConcurrentHashMap 优势大
  • 一致性要求:强一致性通常需要更多同步,性能更低

比如说:

  • 键值随机分布的场景(线程很少操作相同的 key),无锁实现和 ConcurrentHashMap 性能接近
  • 热点数据集中的场景(多线程频繁操作少量 key),ConcurrentHashMap 的细粒度锁更稳定
  • 读操作为主的场景,读写锁和 ConcurrentHashMap 都表现良好
  • 极端写多的场景,可能需要分片或其他特殊设计

实战案例:生产环境中的 HashMap 并发问题

案例 1:商品缓存系统的并发崩溃

我们的商品服务系统用 HashMap 缓存商品信息,初版代码:

public class ProductCacheService {
    // 错误示范:多线程环境下有安全隐患
    private final Map<String, Product> productCache = new HashMap<>();

    public Product getProduct(String id) {
        // 双重检查锁也无法解决HashMap自身的线程安全问题
        Product product = productCache.get(id);
        if (product == null) {
            synchronized (this) {
                product = productCache.get(id);
                if (product == null) {
                    product = loadProductFromDb(id);
                    productCache.put(id, product); // 线程不安全的操作
                }
            }
        }
        return product;
    }
}

上线后系统时不时出现诡异的空指针异常。开始以为是业务逻辑问题,但问题复现极不稳定。某天系统负载高峰期,突然大量请求响应缓慢,CPU 飙升到 100%。

排查过程:

  1. 使用jstack -l <pid>抓取线程堆栈,发现大量线程卡在 HashMap.get()方法上
  2. jmap -dump:live,format=b,file=heap.bin <pid>获取堆转储文件
  3. 通过 VisualVM 分析转储文件,发现 HashMap 内部链表存在环形引用
  4. 问题确诊:并发修改 HashMap 导致的环形链表

解决方案简单粗暴:

public class ProductCacheService {
    // 修复:使用ConcurrentHashMap
    private final Map<String, Product> productCache = new ConcurrentHashMap<>();

    public Product getProduct(String id) {
        return productCache.computeIfAbsent(id, this::loadProductFromDb);
    }

    private Product loadProductFromDb(String id) {
        // 从数据库加载商品
        return dbService.getProduct(id);
    }
}

改用 ConcurrentHashMap 后:

  1. 代码更简洁,直接用 computeIfAbsent 原子操作
  2. 彻底解决了线程安全问题
  3. 性能反而因为减少了锁竞争而提升了

案例 2:统计分析系统的一致性保证

我们的数据统计系统需要生成完整的时间点快照报表,使用 ConcurrentHashMap 后发现问题:报表数据总是不一致——有时漏数据,有时多数据。

问题复现过程:

  1. 使用 JMeter 模拟多线程并发读(模拟报表生成)和定时写(模拟数据更新)
  2. 通过日志记录操作时间戳,发现报表遍历期间 HashMap 结构被修改
  3. 进一步分析发现,ConcurrentHashMap 的迭代器是"弱一致性"的,允许遍历期间 Map 被修改

问题根源:ConcurrentHashMap 提供的是最终一致性(允许遍历时看到部分修改),而报表需要在某个时间点的完整一致快照。

解决方案:使用读写锁实现强一致性视图:

public class StatisticsAnalyzer {
    private final Map<String, AnalysisData> dataMap = new HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    public List<AnalysisData> generateReport() {
        lock.readLock().lock();
        try {
            // 在一个原子操作中获取完整快照
            return new ArrayList<>(dataMap.values());
        } finally {
            lock.readLock().unlock();
        }
    }

    public void updateData(String key, AnalysisData data) {
        lock.writeLock().lock();
        try {
            dataMap.put(key, data);
        } finally {
            writeLock.unlock();
        }
    }

    // 批量更新,保证原子性
    public void batchUpdate(Map<String, AnalysisData> updates) {
        lock.writeLock().lock();
        try {
            dataMap.putAll(updates);
        } finally {
            writeLock.unlock();
        }
    }
}

这个方案确保了报表数据的强一致性,满足了业务需求。虽然并发性能不如 ConcurrentHashMap,但在我们的场景下读多写少,性能完全够用。

总结

HashMap 的线程安全性问题及各种解决方案:

实现方式优点缺点适用场景一致性模型
HashMap单线程性能最佳非线程安全,可能丢数据、死循环仅适用于单线程无线程安全保证
Collections.synchronizedMap实现简单,保证线程安全全局锁,性能差并发量低的场景强一致性(需手动同步迭代器)
ConcurrentHashMap细粒度锁,并发性能好实现复杂,一致性较弱大多数并发场景最终一致性(遍历可能看到部分修改)
Hashtable简单直接性能最差,已过时不推荐使用强一致性
读写锁实现读操作并发,写操作串行全局写锁,写并发受限读多写少、需强一致性强一致性
无锁/低锁实现低竞争下并发性能好实现复杂,高竞争下性能下降低竞争高并发场景通常是最终一致性
不可变 Map绝对线程安全,无需同步修改成本高,创建新对象读多写极少场景绝对一致(不可变)

实际开发中的选择建议:

  1. 默认首选 ConcurrentHashMap,适合绝大多数并发场景
  2. 需要强一致性视图时,考虑读写锁实现
  3. 读多写极少场景,可考虑不可变集合
  4. 除非你是并发编程专家,否则不要自己实现无锁数据结构