ConcurrentHashMap源码阅读

参考:https://www.bilibili.com/video/BV1yT411H7YK

一、关于ConcurrentHashMap

ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它是对HashMap的并发扩展。它提供了高效的并发读写操作,使得多个线程可以同时访问和修改哈希表,而无需显式地进行同步。

在这里插入图片描述

ConcurrentHashMap 是一种线程安全的高效Map集合,底层数据结构:

  • JDK1.7底层采用分段的数组+链表实现

  • JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。

JDK 1.7

数据结构
在这里插入图片描述

  • 提供了一个segment数组,在初始化ConcurrentHashMap 的时候可以指定数组的长度,默认是16,一旦初始化之后中间不可扩容
  • 在每个segment中都可以挂一个HashEntry数组,数组里面可以存储具体的元素,HashEntry数组是可以扩容的
  • 在HashEntry存储的数组中存储的元素,如果发生冲突,则可以挂单向链表

存储流程

在这里插入图片描述

  1. 先去计算key的hash值,然后确定segment数组下标
  2. 再通过hash值确定hashEntry数组中的下标存储数据
  3. 在进行操作数据的之前,会先判断当前segment对应下标位置是否有线程进行操作,为了线程安全使用的是ReentrantLock进行加锁,如果获取锁失败会使用cas自旋锁进行尝试

JDK1.8

在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表

采用 CAS + Synchronized来保证并发安全进行实现

  • CAS控制数组节点的添加

  • synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题 , 效率得到提升

在这里插入图片描述

二、类成员变量

非常抱歉,让我用中文再解释一遍。

以下是JDK 1.8中ConcurrentHashMap源码中的类成员变量,并附有说明:

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
    implements ConcurrentMap<K, V>, Serializable {
    
    // 默认初始容量
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    // 默认装载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 默认并发级别
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    // 最大容量,必须是2的幂次方
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 链表转换为红黑树的阈值
    static final int TREEIFY_THRESHOLD = 8;
    // 红黑树转换为链表的阈值
    static final int UNTREEIFY_THRESHOLD = 6;
    // 不进行树化的最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;

    // 哈希桶数组,存储键值对
    transient volatile Node<K,V>[] table;
    // 下一个扩容阶段的哈希桶数组
    private transient volatile Node<K,V>[] nextTable;
    // 基本计数器,用于统计键值对的数量
    private transient volatile long baseCount;
    // 控制变量,用于控制并发扩容和其他操作
    private transient volatile int sizeCtl;
}

这些变量的作用:

  1. DEFAULT_INITIAL_CAPACITY:默认初始容量
    这个变量定义了ConcurrentHashMap的默认初始容量,即在没有指定初始容量的情况下使用的容量大小。默认值为16。

  2. DEFAULT_LOAD_FACTOR:默认装载因子
    装载因子是指哈希表在达到多满时进行扩容的阈值比例。这个变量定义了ConcurrentHashMap的默认装载因子,即在没有指定装载因子的情况下使用的比例值。默认值为0.75。

  3. DEFAULT_CONCURRENCY_LEVEL:默认并发级别
    并发级别是指可以同时更新的线程数。这个变量定义了ConcurrentHashMap的默认并发级别,即在没有指定并发级别的情况下使用的级别。默认值为16。

  4. MAXIMUM_CAPACITY:最大容量
    这个变量定义了ConcurrentHashMap哈希表的最大容量。由于哈希表的容量必须是2的幂次方,所以最大容量为2^30。

  5. TREEIFY_THRESHOLD:链表转换为红黑树的阈值
    当哈希桶中的链表长度达到这个阈值时,链表将被转换为红黑树,以提高查找性能。默认值为8。

  6. UNTREEIFY_THRESHOLD:红黑树转换为链表的阈值
    当红黑树中的节点数量小于这个阈值时,红黑树将被转换回链表结构。默认值为6。

  7. MIN_TREEIFY_CAPACITY:不进行树化的最小容量
    当哈希桶的容量小于这个值时,不会进行树化操作,即不会将链表转换为红黑树。默认值为64。

  8. table:哈希桶数组,存储键值对
    这个变量是ConcurrentHashMap的核心数据结构,用于存储键值对。它是一个哈希桶数组,每个桶存储一个链表或红黑树结构。

  9. nextTable:下一个扩容阶段的哈希桶数组
    在哈希表进行扩容时,会创建一个新的哈希桶数组,nextTable变量用于指向这个新的数组。

  10. baseCount:基本计数器,用于统计键值对的数量
    这个变量用于记录ConcurrentHashMap中存储的键值对的数量,它是一个基本计数器。

  11. sizeCtl:控制变量,用于控制并发扩容和其他操作
    这个变量用于控制并发扩容和其他操作,它的值会根据不同的情况进行相应的调整。

这些变量在ConcurrentHashMap的实现中起着关键的作用,控制着容量、并发级别、扩容机制以及存储键值对的数据结构等方面。

三、构造方法

JDK 1.8中ConcurrentHashMap源码中的构造方法:

  1. 默认构造方法:
public ConcurrentHashMap() {
    // 使用默认初始容量、默认装载因子和默认并发级别创建ConcurrentHashMap
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}

这个构造方法使用默认的初始容量(16)、默认的装载因子(0.75)和默认的并发级别(16)来创建ConcurrentHashMap。

  1. 指定初始容量的构造方法:
public ConcurrentHashMap(int initialCapacity) {
    // 使用指定初始容量、默认装载因子和默认并发级别创建ConcurrentHashMap
    this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}

这个构造方法使用指定的初始容量来创建ConcurrentHashMap,同时使用默认的装载因子(0.75)和默认的并发级别(16)。

  1. 指定初始容量和装载因子的构造方法:
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    // 使用指定初始容量、指定装载因子和默认并发级别创建ConcurrentHashMap
    this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}

这个构造方法使用指定的初始容量和装载因子来创建ConcurrentHashMap,同时使用默认的并发级别(16)。

  1. 指定初始容量、装载因子和并发级别的构造方法:
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
    // 根据指定的初始容量、装载因子和并发级别来创建ConcurrentHashMap
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) {
        throw new IllegalArgumentException();
    }
    // 省略了一些初始化逻辑...
}

这个构造方法使用指定的初始容量、装载因子和并发级别来创建ConcurrentHashMap。在创建之前,会进行一些参数的检查,确保装载因子大于0,初始容量非负,以及并发级别大于0。

这些构造方法提供了不同的选项来创建ConcurrentHashMap,可以根据需求选择适合的构造方法。

四、添加元素的方法

ConcurrentHashMap中添加元素的方法有put方法和putAll方法:

public V put(K key, V value) {
    return putVal(key, value, false);
}

/**
 * Implementation for put and putIfAbsent
 */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null)
        throw new NullPointerException(); // 检查键和值是否为null,如果是,则抛出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;
}

/**
 * Copies all of the mappings from the specified map to this one.
 * These mappings replace any mappings that this map had for any of the
 * keys currently in the specified map.
 *
 * @param m mappings to be stored in this map
 */
public void putAll(Map<? extends K, ? extends V> m) {
    tryPresize(m.size()); // 预调整桶数组的大小
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        putVal(e.getKey(), e.getValue(), false); // 逐个添加映射到当前map中
}

这些方法的主要步骤包括:

  • put方法:
    1. 参数检查:检查键和值是否为null,如果是,则抛出NullPointerException异常。
    2. 计算哈希值:根据键的哈希码计算出哈希值。
    3. 遍历桶数组:根据哈希值和桶数组的长度确定要放置元素的桶位置。
    4. 桶为空:如果桶为空,则通过CAS操作尝试将新节点放置到桶中。
    5. 桶正在扩容:如果桶正在进行扩容操作,则帮助进行扩容。
    6. 遍历这段代码是ConcurrentHashMap类中的put方法和putAll方法的实现。
public V put(K key, V value) {
    return putVal(key, value, false);
}

这个方法用于将键值对放入ConcurrentHashMap中。它调用了putVal方法,将onlyIfAbsent参数设置为false,表示不仅仅在键不存在时才添加。

接下来,让我们看一下putVal方法的实现:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null)
        throw new NullPointerException(); // 检查键和值是否为null,如果是,则抛出NullPointerException异常
    int hash = spread(key.hashCode()); // 计算键的哈希值
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        // ...
    }
    addCount(1L, binCount); // 更新计数器
    return null;
}

这个方法是put方法的实际实现:

  1. 它首先检查键和值是否为null,如果是,则抛出NullPointerException异常。
  2. 然后,它使用spread方法计算键的哈希值。
  3. 接下来,它使用一个无限循环来遍历桶数组并找到要放置元素的位置。在循环中,它会根据不同的情况执行不同的操作,包括:
    • 如果桶为空,则使用CAS操作将新节点放入桶中。
    • 如果桶正在进行扩容操作,则帮助进行扩容。
    • 如果桶中已经存在节点,则根据节点类型执行不同的操作:如果是链表节点,则遍历链表找到对应的节点并更新值;如果是红黑树节点,则在树中插入或更新节点。
  4. 最后,它会更新计数器并返回null

接下来,让我们来看一下putAll方法:

public void putAll(Map<? extends K, ? extends V> m) {
    tryPresize(m.size()); // 预调整桶数组的大小
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        putVal(e.getKey(), e.getValue(), false); // 逐个添加映射到当前map中
}

这个方法用于将另一个Map中的所有映射添加到当前的ConcurrentHashMap中。它首先调用tryPresize方法来预调整桶数组的大小,然后使用循环逐个将映射添加到当前的ConcurrentHashMap中。

五、删除元素

当涉及到HashMap的remove操作时,这段代码实现了remove方法。remove方法通过调用replaceNode方法来实现节点的替换和删除。

public V remove(Object key) {
    return replaceNode(key, null, null);
}

/**
 * 实现四个公共的remove/replace方法:
 * 根据cv的匹配情况,将节点的值替换为v。如果结果值为null,则删除节点。
 */
final V replaceNode(Object key, V value, Object cv) {
    int hash = spread(key.hashCode());
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 检查tab是否为null或长度为0,或者tab[i]为null
        if (tab == null || (n = tab.length) == 0 ||
            (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;
        // 如果tab[i]的hash值为MOVED,则帮助进行迁移操作
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            boolean validated = false;
            // 对节点f进行同步操作
            synchronized (f) {
                // 再次检查tab[i]是否为f
                if (tabAt(tab, i) == f) {
                    // 如果fh >= 0,表示f是普通节点
                    if (fh >= 0) {
                        validated = true;
                        // 遍历链表或红黑树
                        for (Node<K,V> e = f, pred = null;;) {
                            K ek;
                            // 判断节点e的hash值和key是否相等
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                V ev = e.val;
                                // 如果cv为null,或cv等于ev,或cv与ev相等,则执行替换或删除操作
                                if (cv == null || cv == ev ||
                                    (ev != null && cv.equals(ev))) {
                                    oldVal = ev;
                                    if (value != null)
                                        e.val = value;
                                    else if (pred != null)
                                        pred.next = e.next;
                                    else
                                        setTabAt(tab, i, e.next);
                                }
                                break;
                            }
                            pred = e;
                            if ((e = e.next) == null)
                                break;
                        }
                    }
                    // 如果f是TreeBin类型,则表示f是红黑树
                    else if (f instanceof TreeBin) {
                        validated = true;
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> r, p;
                        // 在红黑树中查找指定的节点
                        if ((r = t.root) != null &&
                            (p = r.findTreeNode(hash, key, null)) != null) {
                            V pv = p.val;
                            // 如果cv为null,或cv等于pv,或cv与pv相等,则执行替换或删除操作
                            if (cv == null || cv == pv ||
                                (pv != null && cv.equals(pv))) {
                                oldVal = pv;
                                if (value != null)
                                    p.val = value;
                                else if (t.removeTreeNode(p))
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
            }
            if (validated) {
                // 如果成功替换或删除节点的值,则根据value的情况更新计数器并返回旧的值
                if (oldVal != null) {
                    if (value == null)
                        addCount(-1L, -1);
                    return oldVal;
                }
                break;
            }
        }
    }
    return null;
}

当调用remove方法时,它会调用replaceNode方法来执行删除操作。下面是replaceNode方法的逻辑解释:

  1. 首先,根据传入的键key计算出哈希值hash
  2. 进入一个无限循环,遍历哈希表中与hash对应的位置上的节点。
  3. 如果当前位置上的节点fnull,则跳出循环。
  4. 如果当前位置上的节点的hash值为MOVED,表示正在进行扩容操作,需要帮助进行迁移操作。
  5. 如果当前位置上的节点是普通节点(非红黑树),则进行以下操作:
    • 遍历节点链表或红黑树,查找与给定的键key相等的节点。
    • 如果找到匹配的节点e,并且满足条件:cvnullcv与节点的值ev相等、或者cvev相等,则执行替换或删除操作。
    • 如果值被替换或删除,则更新旧值oldVal,如果当前操作是删除操作,则更新计数器。
    • 跳出循环。
  6. 如果当前位置上的节点是红黑树,表示当前节点是TreeBin类型,则进行以下操作:
    • 在红黑树中查找与给定的键key相等的节点。
    • 如果找到匹配的节点p,并且满足条件:cvnullcv与节点的值pv相等、或者cvpv相等,则执行替换或删除操作。
    • 如果值被替换或删除,则更新旧值oldVal,如果当前操作是删除操作,则更新计数器。
    • 跳出循环。
  7. 如果成功替换或删除了节点的值,则根据value的情况更新计数器,并返回旧的值。
  8. 如果没有找到匹配的节点,则继续循环遍历哈希表中的下一个位置。
  9. 如果遍历完整个哈希表都没有找到匹配的节点,则返回null表示没有进行删除操作。

通过遍历哈希表中的节点链表或红黑树,找到与给定键相等的节点,并根据条件执行替换或删除操作。它使用了同步来确保在并发情况下的一致性,并根据操作的结果更新计数器并返回旧的值。

clear()方法,用于清空哈希表。它通过遍历哈希表中的节点数组,并对每个位置上的节点进行删除操作。

public void clear() {
    long delta = 0L; // 负数的删除计数
    int i = 0;
    Node<K,V>[] tab = table;
    // 遍历哈希表中的节点数组
    while (tab != null && i < tab.length) {
        int fh;
        Node<K,V> f = tabAt(tab, i);
        // 如果节点为null,说明当前位置无节点,继续下一个位置
        if (f == null)
            ++i;
        // 如果节点的hash值为MOVED,表示正在进行扩容操作,需要帮助进行迁移操作
        else if ((fh = f.hash) == MOVED) {
            tab = helpTransfer(tab, f);
            i = 0; // 重新开始
        }
        else {
            // 对节点进行同步操作
            synchronized (f) {
                // 再次检查tab[i]是否为f
                if (tabAt(tab, i) == f) {
                    Node<K,V> p = (fh >= 0 ? f :
                                   (f instanceof TreeBin) ?
                                   ((TreeBin<K,V>)f).first : null);
                    // 遍历链表或红黑树,对每个节点进行删除计数,并将节点置为null
                    while (p != null) {
                        --delta;
                        p = p.next;
                    }
                    setTabAt(tab, i++, null);
                }
            }
        }
    }
    // 如果删除计数不为0,则根据计数更新计数器
    if (delta != 0L)
        addCount(delta, -1);
}

clear方法的逻辑如下:

  1. 初始化一个变量delta,用于记录删除操作的计数,初始值为0。
  2. 初始化变量i为0,表示当前遍历的节点数组的索引。
  3. 获取哈希表中的节点数组tab
  4. 进入一个循环,条件是节点数组tab不为null且当前索引i小于节点数组的长度。
  5. 在循环中,首先获取节点数组tab中索引为i的节点f
  6. 如果节点f为null,说明当前位置无节点,将索引i加1,继续下一个位置。
  7. 如果节点f的哈希值fh等于MOVED,表示当前节点正在进行扩容操作,需要帮助进行迁移操作。
    • 调用helpTransfer方法,将节点数组tab和当前节点f作为参数,执行帮助迁移操作。
    • 将索引i重新设置为0,重新开始遍历节点数组。
  8. 如果节点f不为null且哈希值fh不等于MOVED,表示当前节点为普通节点。
    • 获取当前节点f的同步锁。
    • 再次检查节点数组tab中索引为i的节点是否为f,避免在获取锁之前节点发生了变化。
    • 如果节点数组tab中索引为i的节点仍然为f,则执行以下操作:
      • 根据节点的哈希值判断节点类型:
        • 如果节点的哈希值fh大于等于0,表示当前节点为普通节点。
          • 将节点f赋值给变量p
        • 如果节点是TreeBin类型的节点,表示当前节点为红黑树节点。
          • 将红黑树节点ffirst节点赋值给变量p,即红黑树的第一个节点。
      • 遍历链表或红黑树,对每个节点进行删除操作:
        • 每遍历到一个节点,将删除计数delta减1。
        • 将当前节点的下一个节点赋值给变量p,继续遍历下一个节点。
      • 将节点数组tab中索引为i的位置置为null,表示删除节点。
  9. 结束同步块。
  10. 将索引i加1,继续下一个位置的遍历。
  11. 如果删除计数delta不等于0,表示执行了删除操作:
    • 调用addCount方法,将删除计数delta和-1作为参数,根据计数更新计数器。
  12. 完成循环遍历节点数组,清空操作结束。

clear方法的主要逻辑是遍历哈希表中的节点数组,对每个位置上的节点进行删除操作。在删除过程中,使用同步锁来确保操作的原子性,同时通过计数器记录删除的节点数量,并根据计数更新计数器的值。最后,如果有节点被删除,则根据删除计数更新计数器。通过这样的逻辑,clear方法实现了清空哈希表的功能。

六、获取元素

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. 计算给定键的哈希值并扩散(spread)。
  2. 获取哈希表中的节点数组tab
  3. 检查节点数组tab是否为null,节点数组的长度n是否大于0,以及索引为(n - 1) & h的节点e是否为null。
  4. 如果存在节点e
    • 检查节点e的哈希值eh是否与扩散后的哈希值h相等。
      • 如果相等,比较键ek是否与给定键key相等。如果相等,则返回节点e的值。
    • 如果节点的哈希值eh小于0,表示节点是树节点。
      • 在树节点e中查找键值对,并返回找到的值。
    • 遍历节点e的链表,查找键值对。
      • 如果哈希值相等,比较键ek是否与给定键key相等。如果相等,则返回节点e的值。
  5. 如果没有找到匹配的键值对,则返回null。

七、其他方法

size()方法用于返回ConcurrentHashMap中键值对的数量。它的实现如下:

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

该方法首先调用了sumCount()方法,该方法用于获取ConcurrentHashMap当前的计数器值。计数器是通过累加每个节点的计数器值得到的。然后,根据计数器值进行以下处理:

  • 如果计数器值n小于0,则返回0。这是因为在计数器更新的过程中,可能存在瞬态的负值,需要忽略这些负值。
  • 如果计数器值n大于Integer.MAX_VALUE,则返回Integer.MAX_VALUE。这是因为size()方法返回一个int类型的值,因此不能超过Integer.MAX_VALUE
  • 如果计数器值n在0到Integer.MAX_VALUE之间,则将其转换为int类型并返回。

isEmpty()方法的实现,用于判断ConcurrentHashMap是否为空:

public boolean isEmpty() {
    return sumCount() <= 0L; // ignore transient negative values
}

该方法也调用了sumCount()方法获取计数器值,并进行判断:

  • 如果计数器值小于等于0,则返回true,表示ConcurrentHashMap为空。同样,这里忽略了瞬态的负值。
  • 如果计数器值大于0,则返回false,表示ConcurrentHashMap不为空。

这两个方法都依赖于sumCount()方法来获取计数器值,而计数器值反映了当前ConcurrentHashMap中键值对的数量。通过检查计数器值,可以准确地得知ConcurrentHashMap的大小和是否为空。


containsKey(Object key)方法,用于检查ConcurrentHashMap中是否包含指定的键。

public boolean containsKey(Object key) {
    return get(key) != null;
}

该方法通过调用get(key)方法来获取指定键的值,并判断返回的值是否为null。如果值不为null,则说明ConcurrentHashMap中存在该键,返回true;否则,返回false

在内部实现中,get(key)方法会根据给定的键执行一系列操作来查找对应的值,如果找到了匹配的键值对,则返回值;如果未找到匹配的键值对,则返回null。因此,通过判断get(key)的返回值是否为null,可以确定ConcurrentHashMap中是否包含指定的键。

这个方法提供了一种简单的方式来检查ConcurrentHashMap中是否包含指定的键,而无需显式获取值并进行比较。


getOrDefault(Object key, V defaultValue)方法用于获取指定键的值,如果键不存在,则返回默认值。其实现如下:

public V getOrDefault(Object key, V defaultValue) {
    V v;
    return (v = get(key)) == null ? defaultValue : v;
}

get(key)方法来获取指定键的值,并将结果赋给变量v。然后,通过三元表达式判断v是否为null,如果是,则返回默认值defaultValue;否则,返回v


forEach(BiConsumer<? super K, ? super V> action)方法用于对ConcurrentHashMap中的每个键值对执行给定的操作。其实现如下:

public void forEach(BiConsumer<? super K, ? super V> action) {
    if (action == null) throw new NullPointerException();
    Node<K,V>[] t;
    if ((t = table) != null) {
        Traverser<K,V> it = new Traverser<K,V>(t, t.length, 0, t.length);
        for (Node<K,V> p; (p = it.advance()) != null; ) {
            action.accept(p.key, p.val);
        }
    }
}

该方法首先检查传入的操作action是否为null,如果是,则抛出NullPointerException。然后,获取ConcurrentHashMap的内部节点数组table,如果数组不为null,则创建一个Traverser对象it,用于遍历节点数组。接下来,通过循环调用it.advance()方法来获取节点数组中的每个节点,并将键和值传递给actionaccept()方法进行处理。

总结

  1. 内部数据结构:ConcurrentHashMap使用了一种分段锁的机制,内部维护了一个由Node节点组成的数组,每个节点包含一个键-值对。数组的每个元素称为一个"段",每个段都是一个独立的哈希表,有自己的锁。每个段中的节点使用链表或红黑树来解决哈希冲突。

  2. 并发操作:ConcurrentHashMap通过使用分段锁(Segment)来实现并发控制。每个段具有自己的锁,不同的段可以独立地进行读写操作,以提高并发性能。这种设计在多线程环境下减少了锁的竞争范围。

  3. 锁的获取和释放:ConcurrentHashMap使用sync数组来存储段的锁。在进行操作之前,首先根据键的哈希值获取对应的段,然后获取该段的锁。对于读操作,多个线程可以同时获取不同段的锁并进行读取。对于写操作,需要获取对应段的锁,并且可能需要进行锁升级,将段内部的链表结构转化为红黑树结构。

  4. 扩容:ConcurrentHashMap采用了分段扩容的策略。在进行扩容时,会对每个段进行扩容,而不是一次性对整个哈希表进行扩容。这种方式减少了扩容时的冲突和数据迁移的规模,提高了扩容的效率。

  5. 计数器:ConcurrentHashMap使用baseCount字段来维护元素的数量。计数器的更新采用了一种"懒惰"的方式,在需要时才进行精确计算。使用CounterCell类来处理计数器的并发更新,每个段都有一个CounterCell数组来存储计数器的增量。

基于JDK 8的ConcurrentHashMap在并发控制和性能方面进行了优化,采用了分段锁和分段扩容的策略,减少了锁的竞争范围,提高了并发性能。同时,它通过延迟计数器的更新和使用CounterCell来处理计数器的并发更新,减少了计数器的冲突。这使得ConcurrentHashMap适用于高并发的读写操作场景。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。
ConcurrentHashMap是Java中线程安全的哈希表实现。它是通过使用分段锁(Segment)来实现并发访问的。 在ConcurrentHashMap源码中,主要有以下几个核心类和接口: 1. ConcurrentHashMap类:ConcurrentHashMap类是ConcurrentMap接口的实现类,它继承自AbstractMap类。它提供了线程安全的并发访问和更新操作。ConcurrentHashMap内部由一个Segment数组组成,每个Segment都是一个独立的哈希表,它们之间互不影响。 2. Segment类:Segment类是ConcurrentHashMap的内部类,每个Segment对象都相当于一个独立的哈希表。Segment内部使用ReentrantLock来实现分段锁,不同的Segment之间可以并发访问。 3. HashEntry类:HashEntry类是ConcurrentHashMap中存储键值对的节点对象,它包含了键、值和下一个节点的引用。ConcurrentHashMap使用链表来解决哈希冲突,每个Segment中都维护了一个HashEntry数组,数组的每个元素都是一个链表的头节点。 4. ConcurrentHashMap的核心方法:ConcurrentHashMap提供了一系列线程安全的操作方法,包括put、get、remove等。这些方法会根据键的哈希值选择对应的Segment,并在该Segment上加锁进行操作。 总体来说,ConcurrentHashMap通过将整个哈希表分成多个Segment,每个Segment都是一个独立的哈希表,并使用分段锁来实现对不同Segment的并发访问。这种设计在多线程环境下能够提供较好的性能和并发能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

abcccccccccccccccode

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值