`ConcurrentHashMap`:高并发场景下的线程安全 Map 实现原理

ConcurrentHashMap:高并发场景下的线程安全 Map 实现原理

各位观众老爷,今天咱们来聊聊 Java 集合框架里的大佬——ConcurrentHashMap。这玩意儿,说白了,就是个能在高并发环境下安全使用的 Map。但别看它名字平平无奇,背后的实现原理可是相当精彩的。如果你跟我一样,每天都在跟多线程、并发编程打交道,那这篇绝对值得你好好看看。

1. 为什么需要 ConcurrentHashMap

首先,咱们得明白,为啥需要这么个特殊的 Map。普通的 HashMap 好用是好用,速度也快,但是它不是线程安全的。这意味着,在多线程环境下,多个线程同时对 HashMap 进行读写操作,可能会导致数据不一致,甚至程序崩溃。

举个例子,假设咱们有一个 HashMap 存储用户的积分信息:

HashMap<String, Integer> userPoints = new HashMap<>();

// 线程 A
new Thread(() -> {
    userPoints.put("Alice", 100);
    Integer points = userPoints.get("Alice");
    userPoints.put("Alice", points + 50);
}).start();

// 线程 B
new Thread(() -> {
    userPoints.put("Bob", 200);
    Integer points = userPoints.get("Bob");
    userPoints.put("Bob", points + 100);
}).start();

上面的代码在单线程环境下没问题,但是在多线程环境下,userPoints 可能会出现各种问题,比如数据覆盖、数据丢失等等。

为了解决这个问题,Java 提供了 Collections.synchronizedMap() 方法,可以将 HashMap 包装成一个线程安全的 Map。

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

虽然 synchronizedMap 实现了线程安全,但是它的性能却非常糟糕。因为它使用了全局锁,每次只有一个线程能够访问 Map,其他线程必须等待。在高并发场景下,这种全局锁会导致严重的性能瓶颈。

这时候,ConcurrentHashMap 就闪亮登场了。它在保证线程安全的同时,还提供了更好的并发性能。

2. ConcurrentHashMap 的核心思想:分段锁

ConcurrentHashMap 的核心思想是分段锁 (Segment)。它将整个 Map 分成多个小的 Segment,每个 Segment 都有自己的锁。当多个线程访问不同的 Segment 时,它们可以并发执行,而不需要等待其他线程释放锁。

你可以把 ConcurrentHashMap 想象成一个大型的停车场,停车场被分成多个区域(Segment),每个区域都有自己的入口和出口(锁)。不同的车辆(线程)可以同时停放在不同的区域,互不干扰。

在 Java 8 之前,ConcurrentHashMap 的实现是基于 Segment 的。而在 Java 8 及以后,ConcurrentHashMap 的实现发生了很大的变化,不再使用 Segment,而是采用了 CAS (Compare and Swap) + synchronized 来实现更细粒度的锁。

3. Java 8 之前的 ConcurrentHashMap (基于 Segment)

为了理解 ConcurrentHashMap 的演进,我们先来看看 Java 8 之前的实现。

  • 数据结构: ConcurrentHashMap 由一个 Segment 数组组成,每个 Segment 实际上就是一个小的 HashMap
  • 锁: 每个 Segment 都有自己的 ReentrantLock 锁。
  • 并发: 当多个线程访问不同的 Segment 时,它们可以并发执行。
  • put 操作:
    1. 根据 key 的 hash 值,找到对应的 Segment。
    2. 尝试获取 Segment 的锁。
    3. 如果获取成功,将 key-value 放入 Segment 中。
    4. 释放 Segment 的锁。
  • get 操作:
    1. 根据 key 的 hash 值,找到对应的 Segment。
    2. 不需要获取锁,直接从 Segment 中读取数据。 (大多数情况下,get 操作不需要加锁。只有在 Segment 正在扩容时,才需要加锁。)

下面是一个简单的示例代码,展示了 Java 8 之前 ConcurrentHashMap 的结构:

// 假设的 ConcurrentHashMap 实现(Java 8 之前)
class ConcurrentHashMapPreJava8<K, V> {
    static final int DEFAULT_SEGMENT_SIZE = 16; // 默认的 Segment 数量

    final Segment<K, V>[] segments;

    @SuppressWarnings("unchecked")
    public ConcurrentHashMapPreJava8() {
        segments = (Segment<K, V>[]) new Segment[DEFAULT_SEGMENT_SIZE];
        for (int i = 0; i < DEFAULT_SEGMENT_SIZE; i++) {
            segments[i] = new Segment<>();
        }
    }

    static class Segment<K, V> {
        final ReentrantLock lock = new ReentrantLock();
        final HashMap<K, V> map = new HashMap<>();

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

        V get(K key) {
            return map.get(key);
        }
    }

    V put(K key, V value) {
        int segmentIndex = getSegmentIndex(key);
        return segments[segmentIndex].put(key, value);
    }

    V get(K key) {
        int segmentIndex = getSegmentIndex(key);
        return segments[segmentIndex].get(key);
    }

    private int getSegmentIndex(K key) {
        // 根据 key 的 hash 值,计算出 Segment 的索引
        int hash = key.hashCode();
        return (hash >>> 16) & (segments.length - 1); // 简单示例,实际实现更复杂
    }
}

优点:

  • 并发性能好,多个线程可以并发访问不同的 Segment。

缺点:

  • Segment 的数量是固定的,无法动态调整。
  • 如果多个线程访问同一个 Segment,仍然会存在锁竞争。
  • 代码实现相对复杂。

4. Java 8 及以后的 ConcurrentHashMap (基于 CAS + synchronized)

Java 8 对 ConcurrentHashMap 进行了重大的改进,不再使用 Segment,而是采用了更细粒度的锁机制。

  • 数据结构: ConcurrentHashMap 由一个 Node 数组组成,每个 Node 存储一个 key-value 对。
  • 锁: 锁的粒度更细,只锁住 Node 数组中的一个桶 (bucket)。
  • 并发: 使用 CAS (Compare and Swap) 操作来保证线程安全,减少锁的竞争。
  • put 操作:
    1. 根据 key 的 hash 值,找到对应的桶。
    2. 如果桶为空,使用 CAS 操作将新的 Node 放入桶中。
    3. 如果桶不为空,锁住桶,然后将新的 Node 放入桶中。
    4. 如果链表长度超过阈值 (TREEIFY_THRESHOLD),将链表转换为红黑树。
  • get 操作:
    1. 根据 key 的 hash 值,找到对应的桶。
    2. 遍历桶中的链表或红黑树,找到对应的 Node。
    3. 返回 Node 的 value。

下面是一个简化的 Java 8 ConcurrentHashMap 的 put 操作示例:

// 假设的 ConcurrentHashMap 实现(Java 8 之后,部分代码)
class ConcurrentHashMapJava8<K, V> {
    static final int DEFAULT_CAPACITY = 16;
    static final int TREEIFY_THRESHOLD = 8; // 链表转红黑树的阈值

    transient volatile Node<K, V>[] table;

    static class Node<K, V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K, V> next;

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

    @SuppressWarnings("unchecked")
    public ConcurrentHashMapJava8() {
        table = (Node<K, V>[]) new Node[DEFAULT_CAPACITY];
    }

    V put(K key, V value) {
        int hash = key.hashCode();
        int i = (hash & (table.length - 1)); // 计算桶的索引

        if (table[i] == null) {
            // 桶为空,使用 CAS 操作放入新的 Node
            if (casTabAt(table, i, null, new Node<>(hash, key, value, null)))
                return null; // 成功
        } else {
            synchronized (table[i]) { // 锁住桶
                Node<K, V> p = table[i];
                Node<K, V> prev = null;
                while (p != null) {
                    if (p.hash == hash && p.key.equals(key)) {
                        // 找到相同的 key,更新 value
                        V oldValue = p.val;
                        p.val = value;
                        return oldValue;
                    }
                    prev = p;
                    p = p.next;
                }
                // 没有找到相同的 key,将新的 Node 放入链表尾部
                if (prev != null) {
                    prev.next = new Node<>(hash, key, value, null);
                } else {
                    table[i] = new Node<>(hash, key, value, null);
                }
                // 如果链表长度超过阈值,转换为红黑树 (省略)
            }
        }
        return null;
    }

    // 使用 Unsafe 类的 CAS 操作
    static final <K, V> boolean casTabAt(Node<K, V>[] tab, int i,
                                            Node<K, V> c, Node<K, V> v) {
        return UNSAFE.compareAndSwapObject(tab, ((long) i << ASHIFT) + ABASE, c, v);
    }

    // ... 其他方法和 Unsafe 类的初始化
    private static final sun.misc.Unsafe UNSAFE;
    private static final long ABASE;
    private static final int ASHIFT;

    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = Node[].class;
            ABASE = UNSAFE.arrayBaseOffset(k);
            int scale = UNSAFE.arrayIndexScale(k);
            if ((scale & (scale - 1)) != 0)
                throw new Error("data type scale not a power of two");
            ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
        } catch (Exception e) {
            throw new Error(e);
        }
    }

}

优点:

  • 锁的粒度更细,并发性能更好。
  • 使用 CAS 操作,减少了锁的竞争。
  • 支持动态扩容。
  • 链表转红黑树,提高了查询效率。

缺点:

  • 代码实现更加复杂。

5. ConcurrentHashMap 的重要特性

除了并发性能,ConcurrentHashMap 还有一些重要的特性:

  • 线程安全: 保证在多线程环境下数据的正确性。
  • 弱一致性: ConcurrentHashMap 的迭代器是弱一致性的。这意味着,在迭代过程中,如果 Map 被修改,迭代器不一定会反映出最新的修改。但是,迭代器保证能够遍历到 Map 中所有已经存在的元素。
  • 不允许 null key 和 null value: ConcurrentHashMap 不允许 key 或 value 为 null。如果尝试 put 一个 null key 或 null value,会抛出 NullPointerException

6. ConcurrentHashMap 的应用场景

ConcurrentHashMap 广泛应用于各种高并发场景,例如:

  • 缓存: 可以使用 ConcurrentHashMap 来存储缓存数据,提高应用程序的响应速度。
  • 会话管理: 可以使用 ConcurrentHashMap 来存储用户的会话信息。
  • 计数器: 可以使用 ConcurrentHashMap 来实现高并发的计数器。
  • 消息队列: 可以使用 ConcurrentHashMap 来存储消息队列中的消息。

7. ConcurrentHashMap 的一些优化技巧

在使用 ConcurrentHashMap 时,可以采用一些优化技巧来提高性能:

  • 合理设置初始容量: ConcurrentHashMap 的初始容量会影响其性能。如果能够预估 Map 的大小,最好设置一个合适的初始容量,避免频繁的扩容。
  • 选择合适的并发级别: 并发级别指的是 ConcurrentHashMap 中锁的数量。在 Java 8 之前,可以通过构造函数来设置并发级别。在 Java 8 及以后,并发级别不再是一个显式的参数,而是由 ConcurrentHashMap 内部自动管理。
  • 避免使用 null key 和 null value: ConcurrentHashMap 不允许 null key 和 null value,避免因此导致的异常。
  • 了解弱一致性: 在使用迭代器时,需要了解 ConcurrentHashMap 的弱一致性特性。

8. ConcurrentHashMap 与其他线程安全 Map 的比较

特性 ConcurrentHashMap Collections.synchronizedMap() Hashtable
线程安全
并发性能
锁的粒度 粗 (全局锁) 粗 (全局锁)
是否允许 null 允许
数据结构 Node 数组 + 链表/红黑树 HashMap + 全局锁 Hashtable
迭代器一致性 弱一致性 强一致性 强一致性

9. 总结

ConcurrentHashMap 是一个非常强大的线程安全 Map 实现,它在高并发场景下提供了优异的性能。通过理解其核心思想和实现原理,我们可以更好地使用它,并将其应用到各种实际场景中。

总而言之,ConcurrentHashMap 就像一位身经百战的将军,在并发战场上披荆斩棘,为我们的应用程序保驾护航。掌握了它的使用方法和原理,你也能成为并发编程领域的佼佼者!希望这篇文章能帮助你更好地理解 ConcurrentHashMap,并在实际工作中灵活运用。 码字不易,如果觉得有用,别忘了点个赞哦!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注