首页 > 其他分享 >ConcurrentHashMap 详解

ConcurrentHashMap 详解

时间:2024-12-02 09:02:42浏览次数:6  
标签:Node ConcurrentHashMap 数组 int 详解 tab null

在 Java 的并发编程中,ConcurrentHashMap 是一个非常重要的数据结构。它位于 java.util.concurrent 包中,提供了线程安全的哈希表实现,能够在多线程环境下高效地进行读写操作。本文将深入探讨 ConcurrentHashMap 的内部实现、线程安全机制以及在不同 JDK 版本中的变化。

1 ConcurrentHashMap 的线程安全机制

ConcurrentHashMap 的设计初衷是为了解决 HashMap 在多线程环境下扩容时可能导致的 CPU 占用接近 100% 的问题。HashMap 本身并不是线程安全的,虽然可以通过 Collections.synchronizedMap(Map<K,V> m) 将其包装成线程安全的 Map,但这种方式在高并发场景下性能较差。

ConcurrentHashMap 通过锁分段技术(JDK 1.7)或 CAS 操作(JDK 1.8)来实现线程安全,大大提高了并发性能。

2 JDK 1.7 中的 ConcurrentHashMap

在 JDK 1.7 中,ConcurrentHashMap 采用了一种称为分段锁Lock Striping)的机制。这种机制将整个哈希表分成多个段(Segment),每个段都独立加锁。读取操作不需要锁,写入操作仅锁定相关的段。这种设计减少了锁冲突的几率,从而提高了并发性能。

2.1 分段锁的优势

  • 高并发吞吐量:在并发环境下,分段锁机制能够实现更高的吞吐量。
  • 低性能损失:在单线程环境下,分段锁机制只损失非常小的性能。

2.2 分段锁的实现

  • Segment 数组ConcurrentHashMap 包含一个 Segment 数组,每个 Segment 类似于一个小的 HashMap
  • HashEntry 数组:每个 Segment 包含一个 HashEntry 数组,用于存储键值对数据。
  • 锁机制:每个 Segment 持有一把可重入锁(ReentrantLock),当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

2.3 分段锁的结构

ConcurrentHashMap 的结构可以看作是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表(即 Segment)。每个 Segment 独立加锁,从而实现并发操作。

2.4 读写过程

  • get 方法

    1. 为输入的 Key 做 Hash 运算,得到 hash 值。
    2. 通过 hash 值,定位到对应的 Segment 对象。
    3. 再次通过 hash 值,定位到 Segment 当中数组的具体位置。
  • put 方法

    1. 为输入的 Key 做 Hash 运算,得到 hash 值。
    2. 通过 hash 值,定位到对应的 Segment 对象。
    3. 获取可重入锁。
    4. 再次通过 hash 值,定位到 Segment 当中数组的具体位置。
    5. 插入或覆盖 HashEntry 对象。
    6. 释放锁。

3 JDK 1.8 中的 ConcurrentHashMap

在 JDK 1.8 中,ConcurrentHashMap 进行了重大改进,主要体现在以下几个方面:

3.1 放弃分段锁

JDK 1.8 放弃了分段锁机制,转而使用 CAS 操作和 synchronized 关键字来保证并发安全性。整个容器只分为一个 Segment,即 table 数组。

3.2 链表转红黑树

HashMap 一样,当链表长度达到 8 时,链表会转换为红黑树,以提高大量冲突时的查询效率。

3.3 CAS 操作

以某个位置的头结点(链表的头结点或红黑树的 root 结点)为锁,配合自旋+ CAS 避免不必要的锁开销,进一步提升并发性能。

3.4 节点类

JDK 1.8 中的 ConcurrentHashMap 对节点类进行了优化,使用 volatile 关键字保证多线程操作时变量的可见性。

static class Node<K,V> implements Map.Entry<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;
    }
}

4 ConcurrentHashMap 的内部实现

4.1 字段

ConcurrentHashMap 中有几个关键字段:

  • table:存放 Node 的数组,采用懒加载方式,直到第一次插入数据时才会初始化。
  • nextTable:扩容时使用,平时为 null
  • sizeCtl:控制 table 数组的大小,根据是否初始化和是否正在扩容有不同的含义。
    • 当值为负数时: 如果为-1 表示正在初始化,如果为 -N 则表示当前正有 N-1 个线程进行扩容操作;
    • 当值为正数时: 如果当前数组为 null 的话表示 table 在初始化过程中,sizeCtl 表示为需要新建数组的长度;若已经初始化了,表示当前数据容器(table 数组)可用容量,也可以理解成临界值(插入节点数超过了该临界值就需要扩容),具体指为数组的长度n乘以 加载因子 loadFactor;
    • 当值为 0 时,即数组长度为默认初始值。
  • sun.misc.Unsafe U:用于实现 CAS 操作,保证线程安全。

CAS 操作依赖于现代处理器指令集,通过底层的CMPXCHG指令实现。CAS(V,O,N)核心思想为:若当前变量实际值 V 与期望的旧值 O 相同,则表明该变量没被其他线程进行修改,因此可以安全的将新值 N 赋值给变量;若当前变量实际值 V 与期望的旧值 O 不相同,则表明该变量已经被其他线程做了处理,此时将新值 N 赋给变量操作就是不安全的,在进行重试

4.2 节点类

ConcurrentHashMap 中的节点类包括 NodeTreeNodeTreeBinForwardingNode

  • Node:实现了 Map.Entry 接口,主要存放键值对,并具有 next 域。
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    ......
}
  • TreeNode:树节点,继承自 Node 类,用于红黑树的实现。
**
 * Nodes for use in TreeBins
 */
static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
		......
}
  • TreeBin:封装了 TreeNode,实际的 ConcurrentHashMap 数组中存放的是 TreeBin 对象,而不是 TreeNode 对象。
static final class TreeBin<K,V> extends Node<K,V> {
        TreeNode<K,V> root;
        volatile TreeNode<K,V> first;
        volatile Thread waiter;
        volatile int lockState;
        // values for lockState
        static final int WRITER = 1; // set while holding write lock
        static final int WAITER = 2; // set when waiting for write lock
        static final int READER = 4; // increment value for setting read lock
		......
}
  • ForwardingNode:在扩容时出现的特殊节点,用于标记正在被迁移的节点。
static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }
   .....
}

4.3 CAS 操作

ConcurrentHashMap 中,CAS 操作主要用于以下几个方面:

  1. 获取数组元素tabAt 方法用于获取 table 数组中指定索引位置的元素。
  2. 设置数组元素casTabAt 方法用于通过 CAS 操作设置 table 数组中指定索引位置的元素。
  3. 直接设置数组元素setTabAt 方法用于直接设置 table 数组中指定索引位置的元素。

下面我们详细介绍这些方法的实现。

4.3.1 tabAt 方法

tabAt 方法用于获取 table 数组中索引为 iNode 元素。

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
  • U.getObjectVolatile:这是一个 Unsafe 类的方法,用于获取数组中指定索引位置的元素,并保证该操作的可见性。
  • ((long)i << ASHIFT) + ABASE:计算数组中元素的内存地址。ASHIFTABASE 是常量,用于计算数组元素的偏移量。

4.3.2 casTabAt 方法

casTabAt 方法利用 CAS 操作设置 table 数组中索引为 i 的元素。

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
  • U.compareAndSwapObject:这是一个 Unsafe 类的方法,用于执行 CAS 操作。它会比较数组中指定索引位置的元素是否等于 c,如果相等则将其替换为 v,并返回 true;否则返回 false
  • ((long)i << ASHIFT) + ABASE:计算数组中元素的内存地址。

4.3.3 setTabAt 方法

setTabAt 方法用于直接设置 table 数组中索引为 i 的元素。

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
  • U.putObjectVolatile:这是一个 Unsafe 类的方法,用于直接设置数组中指定索引位置的元素,并保证该操作的可见性。
  • ((long)i << ASHIFT) + ABASE:计算数组中元素的内存地址。

4.4 方法

ConcurrentHashMap 是 Java 并发包 (java.util.concurrent) 中的一种线程安全的哈希表实现。它提供了多种方法来支持高效的并发操作。本文将详细介绍 ConcurrentHashMap 的构造方法、初始化方法、插入方法、获取方法、扩容方法以及与大小相关的方法。

4.4.1 构造方法

ConcurrentHashMap 提供了以下五种构造方法:

// 1. 构造一个空的 map,即 table 数组还未初始化,初始化放在第一次插入数据时,默认大小为 16
ConcurrentHashMap()

// 2. 给定 map 的大小
ConcurrentHashMap(int initialCapacity)

// 3. 给定一个 map
ConcurrentHashMap(Map<? extends K, ? extends V> m)

// 4. 给定 map 的大小以及加载因子
ConcurrentHashMap(int initialCapacity, float loadFactor)

// 5. 给定 map 大小,加载因子以及并发度(预计同时操作数据的线程)
ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)

我们来看第 2 种构造方法的源码:

public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

这段代码的逻辑如下:

  1. 如果指定的初始容量小于 0,抛出异常。
  2. 判断指定的初始容量是否超过了允许的最大值,如果超过则取最大值,否则对指定值进行进一步处理。
  3. 将处理后的容量赋值给 sizeCtl

tableSizeFor 方法用于将指定的容量转换为 2 的幂次方数:

private static final int tableSizeFor(int c) {
    int n = c - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

4.4.2 初始化方法 initTable

initTable 方法用于初始化 table 数组:

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(); // 保证只有一个线程正在进行初始化操作
        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); // 计算数组中可用的大小:实际大小 n * 0.75
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

代码逻辑如下:

  1. 如果当前 table 数组还未初始化,多个线程可能会同时进入这个方法。
  2. 通过 CAS 操作将 sizeCtl 改为 -1,表示正在初始化。
  3. 初始化 table 数组,并计算数组中可用的大小。
  4. sizeCtl 更新为新数组的可用大小。

4.4.3 插入方法 put

put 方法调用 putVal 方法来插入键值对:

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;
        }
        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;
}

代码逻辑如下:

  1. 计算键的哈希值。
  2. 如果 table 数组还未初始化,调用 initTable 方法进行初始化。
  3. 如果目标位置为空,使用 CAS 操作插入新节点。
  4. 如果当前正在扩容,调用 helpTransfer 方法协助扩容。
  5. 如果目标位置不为空,根据节点类型(链表或红黑树)插入新节点。
  6. 插入完成后,检查链表长度是否需要转换为红黑树。
  7. 检查当前容量是否需要扩容。

4.4.4 获取方法 get

get 方法用于获取键对应的值:

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;
}

代码逻辑如下:

  1. 计算键的哈希值。
  2. 检查 table 数组是否已初始化,并定位到目标桶。
  3. 如果桶的第一个节点与目标键匹配,直接返回该节点的值。
  4. 如果桶的第一个节点是红黑树节点,调用 find 方法在红黑树中查找。
  5. 否则,遍历链表查找目标键。

4.4.5 扩容方法 transfer

transfer 方法用于扩容 table 数组:

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE;
    if (nextTab == null) {
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false;
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
                                         nextBound = (nextIndex > stride ?
                                                      nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
                i = n;
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            advance = true;
        else {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    if (fh >= 0) {
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    else if (f instanceof TreeBin) {
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>(h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                            (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                            (lc != 0) ? new TreeBin<K,V>(hi) : t;
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}

代码逻辑如下:

  1. 创建一个新的 nextTable,容量为原 table 的两倍。
  2. 遍历原 table 中的每个桶,将桶中的元素复制到 nextTable 中。
  3. 如果桶为空,使用 CAS 操作设置 forwardingNode 节点。
  4. 如果桶中的节点是链表节点,将链表分裂成两个链表,分别放入 nextTable 的对应位置。
  5. 如果桶中的节点是红黑树节点,将红黑树分裂成两个红黑树,分别放入 nextTable 的对应位置。
  6. 完成复制后,将 nextTable 设为新的 table,并更新 sizeCtl

4.4.6 大小相关的方法

ConcurrentHashMap 提供了 sizemappingCount 方法来获取元素的数量:

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}

public long mappingCount() {
    long n = sumCount();
    return (n < 0L) ? 0L : n;
}

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

代码逻辑如下:

  1. size 方法返回 Map 中的元素数量,结果被限制在 Integer.MAX_VALUE 内。
  2. mappingCount 方法返回 Map 中的元素数量,允许返回一个 long 值。
  3. sumCount 方法计算 Map 的实际大小,使用 baseCountcounterCells 数组来跟踪大小。

5 ConcurrentHashMap 的并发控制

ConcurrentHashMap 通过 CAS 操作和 synchronized 关键字来实现并发控制。CAS 操作是一种乐观锁策略,假设每一次操作都不会产生冲突,当且仅当冲突发生时再去尝试。synchronized 关键字在 JDK 1.8 中经过优化,性能与 ReentrantLock 相当,甚至在某些情况下更优。

6 ConcurrentHashMap 的应用示例

假设我们需要构建一个线程安全的高并发统计用户访问次数的功能,ConcurrentHashMap 是一个很好的选择。以下是一个简单的示例:

import java.util.concurrent.ConcurrentHashMap;

public class UserVisitCounter {

    private final ConcurrentHashMap<String, Integer> visitCountMap;

    public UserVisitCounter() {
        this.visitCountMap = new ConcurrentHashMap<>();
    }

    // 用户访问时调用的方法
    public void userVisited(String userId) {
        visitCountMap.compute(userId, (key, value) -> value == null ? 1 : value + 1);
    }

    // 获取用户的访问次数
    public int getVisitCount(String userId) {
        return visitCountMap.getOrDefault(userId, 0);
    }

    public static void main(String[] args) {
        UserVisitCounter counter = new UserVisitCounter();

        // 模拟用户访问
        counter.userVisited("user1");
        counter.userVisited("user1");
        counter.userVisited("user2");

        System.out.println("User1 visit count: " + counter.getVisitCount("user1")); // 输出: User1 visit count: 2
        System.out.println("User2 visit count: " + counter.getVisitCount("user2")); // 输出: User2 visit count: 1
    }
}

在上述示例中:

  • 我们使用了 ConcurrentHashMap 来存储用户的访问次数。
  • 当用户访问时,我们通过 userVisited 方法更新访问次数。
  • 使用 ConcurrentHashMapcompute 方法可以确保原子地更新用户的访问次数。
  • 可以通过getVisitCount方法检索任何用户的访问次数。

7 小结

ConcurrentHashMap 是 Java 并发包中一个高效且线程安全的哈希表实现。它支持完全并发的读取,并且能够在多线程环境下高效地进行写入操作。从 JDK 1.7 到 JDK 1.8,ConcurrentHashMap 的内部实现经历了重大改进,从分段锁机制到 CAS 操作和红黑树的应用,进一步提升了并发性能。

通过本文的介绍,希望读者能够更好地理解 ConcurrentHashMap 的工作原理和应用场景,从而在实际开发中更加高效地使用这一强大的数据结构。

8 思维导图

在这里插入图片描述

9 参考链接

吊打Java面试官之ConcurrentHashMap(线程安全的哈希表)

标签:Node,ConcurrentHashMap,数组,int,详解,tab,null
From: https://blog.csdn.net/gaosw0521/article/details/144024470

相关文章

  • linux ln命令详解
    介绍ln是linux的一个重要命令,它的功能是为某一个文件在另外一个位置建立一个同步的链接。当我们需要在不同的目录,用到相同的文件时,我们不需要在每一个需要的目录下都放一个必须相同的文件,我们只要在某个固定的目录,放上该文件,然后在其它的目录下用ln命令链接(link)它就可以,不必重复......
  • 博主自留二叉树万字长文—>涵盖名词辨析 + 树的两种表示方法 + 所有特殊二叉树 + 图解
    玩转二叉树(概念+图解+例题代码详解)一、树的概念我们知道在计算机什么是树吗?是二月春风似剪刀吗?哈哈哈哈哈哈显然不是我们看下面这张图,可以观察到树的一些特征1、树的特征(1)树是非线性的数据结构,是递归定义的(连通性)(2)子树之间不能有任何交集(无环性)(3)一颗N......
  • shell编程详解,看这一篇就够了
    声明!学习视频来自B站up主泷羽sec有兴趣的师傅可以关注一下,如涉及侵权马上删除文章,笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无关,切勿触碰法律底线,否则后果自负!!!!有兴趣的小伙伴可以点击下面连接进入b站主页B站泷......
  • Linux线程详解
    一、线程的概念        在引入线程之前,进程作为资源分配的最小单位(分配得到了CPU的时间、内存等),操作系统通过调度算法实现多进程并发执行,共用CPU,但由于创建或撤销进程时,系统都要为之分配或回收资源,限制了并发程度的提高。后来,为了减少进程间切换的开销,可把进程作为资......
  • C/C++ 删除字符串中重复的字符并排序算法详解及源码
    删除重复字符算法详解及代码思路创建一个数组,默认所有元素都是0;遍历字符串的每个字符;判断当前字符是否已经出现过,如果没有出现过,则将数组中对应位置设为0,如果当前字符已经出现过,则将数组中对应位置设为1;遍历数组把值为0对应位置的字符加入到结果字符串中;返回结果字符串。代......
  • HickWall 详解
    优质博文:IT-BLOG-CN一、监控分类【1】Tracing调用链:【2】Logging日志:【3】Metrics指标:在应用发布之后,会长时间存在的度量维度。某个接口的请求量、响应时间。Metrics数据模型二、Metirc接入【1】pom.xml中添加metric-client依赖<dependency><groupId>com.ctri......
  • 文件上传漏洞&靶场通关详解
    文件上传漏洞&靶场通关详解什么是文件上传漏洞?大部分网站都拥有上传文件的部分,文件上传漏洞是由于网站开发者对用户上传文件的过滤不够严格,攻击者可以通过这些漏洞上传可执行文件(如木马,恶意脚本和WebShell等等),从而达到随意控制网站的目的。文件上传漏洞有哪些危害?攻击......
  • AES加密算法原理详解
    AES加密:高级加密标准(AES,AdvancedEncryptionStandard)为最常见的对称加密算法(微信小程序加密传输就是用这个加密算法的)。对称加密算法也就是加密和解密用相同的密钥,具体的加密流程如下图:明文p::::info没有经过加密的数据。:::密钥K::::info用来加密明文的密码,在......
  • 使用WebAssembly结合Rust实现高性能Web应用的技术详解
    ......
  • 详解虚数 i^i | 图解虚数 i
    注:本文为“虚数iii”相关文章合辑。机翻,未校。itothepoweri2023-08-0800:00:00Theimaginaryunit......