寒假打卡11-1月24日

Java ConcurrentHashMap 详解——并发安全实现原理

在多线程环境中,HashMap 并不是线程安全的,多个线程同时访问和修改 HashMap 可能会导致数据不一致的问题。为了解决这个问题,Java 提供了线程安全的 ConcurrentHashMap 类。ConcurrentHashMap 通过分段锁(Segmented Locking)和高效的并发算法,确保在高并发场景下的线程安全和高性能。

ConcurrentHashMap 的基本结构

ConcurrentHashMap 基于哈希表实现,与 HashMap 相似,但采用了更复杂的数据结构和锁机制。它通过将数据分成多个段(Segment),每个段维护自己的锁,从而实现高效的并发访问。

ConcurrentHashMap 的重要字段

transient volatile Node<K, V>[] table; // 存储元素的数组
private transient volatile int sizeCtl; // 控制表的初始化和调整大小
private transient volatile long baseCount; // 基本计数变量,用于统计元素数量
private transient volatile int cellsBusy; // 控制 cells 的初始化和更新
private transient volatile CounterCell[] counterCells; // 计数单元数组,用于统计元素数量

put 方法解析

put 方法用于向 ConcurrentHashMap 中添加键值对。如果键已经存在,则更新其对应的值;如果键不存在,则插入新的键值对。

put 方法的主要步骤

  1. 计算哈希值:通过 spread(hash) 方法计算键的哈希值。
  2. 定位桶:通过 tabAt(tab, i) 方法定位桶的索引。
  3. 插入新节点:如果桶为空,则通过 CAS 操作插入新节点;如果桶不为空,则加锁并遍历链表或红黑树,检查键是否已经存在。
  4. 扩容检查:检查是否需要扩容,如果需要则进行扩容。

源码解析

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    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) {
            if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K, V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K, V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K, V>(hash, key, value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K, V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K, V>)f).putTreeVal(hash, key, value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

在上述代码中,putVal 方法首先检查并初始化 table 数组,然后根据哈希值定位桶的索引。如果桶为空,则通过 CAS 操作插入新节点;如果桶不为空,则加锁并遍历链表或红黑树,检查键是否已经存在。如果键存在,则更新其值;如果键不存在,则插入新节点。

get 方法解析

get 方法用于根据键从 ConcurrentHashMap 中获取对应的值。如果键存在,则返回对应的值;如果键不存在,则返回 null

get 方法的主要步骤

  1. 计算哈希值:通过 spread(hash) 方法计算键的哈希值。
  2. 定位桶:通过 tabAt(tab, i) 方法定位桶的索引。
  3. 遍历链表或红黑树:查找键对应的节点,并返回其值。

源码解析

public V get(Object key) {
    Node<K, V>[] tab; Node<K, V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

在上述代码中,get 方法首先通过 spread(hash) 计算哈希值,然后通过 tabAt(tab, i) 方法定位桶的索引,并遍历链表或红黑树查找键对应的节点,最后返回节点的值。

扩容机制

ConcurrentHashMap 中的键值对数量超过阈值时,需要对 table 数组进行扩容。扩容的过程包括:

  1. 创建新数组:创建一个容量是原数组两倍的新数组。
  2. 重新散列:将原数组中的节点重新散列到新数组中。

源码解析

private final Node<K, V>[] initTable() {
    Node<K, V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K, V>[] nt = (Node<K, V>[])new Node<?, ?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

在上述代码中,initTable 方法用于初始化 table 数组,并设置阈值 sizeCtl。在扩容过程中,ConcurrentHashMap 采用分段锁机制,确保多个线程可以并发进行扩容操作。

总结

通过解析 ConcurrentHashMapputget 方法源码,我们可以深入理解其底层逻辑和实现原理。ConcurrentHashMap 通过分段锁和高效的并发算法,确保在高并发场景下的线程安全和高性能。它是多线程环境下处理哈希表的理想选择。

希望通过本篇文章,大家对 Java ConcurrentHashMap 的底层实现有了更深入的了解。在接下来的文章中,我们将继续探讨更多关于 Java 并发编程的知识点,敬请期待!

posted @ 2025-01-24 09:09  aallofitisst  阅读(17)  评论(0)    收藏  举报