Java-HashMap和ConcurrentHashMap的区别

Java-HashMap和ConcurrentHashMap的区别

    • 一、关键区别
      • 1.数据结构
      • 2.线程安全
      • 3.性能
      • 4.扩容机制
    • 二、源码简析
      • 1.并发控制机制
      • 2.数据结构转换:链表转红黑树
      • 3.扩容机制
        • 触发hashMap和concurentHashMap扩容机制的条件
    • 三、putIfAbsent方法computeIfAbsent方法区别

​ 在 Java 8中, HashMap和ConcurrentHashMap都经历了一些重要的改进,使得它们 在性能和线程安全性方面有了 显著的差异

一、关键区别

1.数据结构

  • HashMap (JDK 8):
    1.8之前的
    在这里插入图片描述
    1.8后
    在这里插入图片描述

    数组 + 链表 + 红黑树:Java 8中的HashMap引入了红黑树来优化链表过长的情况。当链表长度超过8时(默认设置),链表会被转换成红黑树,以减少查找时间复杂度,从O(n)降低到O(log n)。这一改变主要目的是提高在哈希冲突较多情况下的性能

  • ConcurrentHashMap (JDK 8):
    1.7中
    在这里插入图片描述
    1.8中
    在这里插入图片描述

    数组 + 链表 + 红黑树:类似于HashMap,ConcurrentHashMap在Java 8中也引入了红黑树来优化性能。但是,ConcurrentHashMap的内部结构更为复杂,它不再使用分段锁(Segment),而是采用CAS(Compare and Swap)操作加上一些其他原子操作和少量的synchronized关键字来实现更细粒度的锁,这大大提升了并发性能

2.线程安全

  • HashMap: 不是线程安全的。在多线程环境下,如果没有外部同步,直接使用HashMap可能会导致数据不一致的问题。
  • ConcurrentHashMap: 是线程安全的。它通过使用CAS算法和synchronized块(只在最小必要时锁定),实现了对部分桶的加锁,而不是像早期版本那样对整个容器加锁,从而在高并发环境下提供了更好的性能

3.性能

  • HashMap: 单线程环境下性能优秀,但在多线程环境下没有提供安全保障,可能导致数据损坏。
  • ConcurrentHashMap: 在多线程环境下性能优越,因为它能更好地控制锁的范围,减少线程之间的竞争,同时在单线程访问时也能保持良好的性能

4.扩容机制

  • HashMap: 在Java 8中,HashMap的扩容仍然是一个相对重量级的操作,虽然它在插入新元素时采用了更高效的尾插法来避免循环链表问题,但扩容时仍需重新分配桶并复制元素
  • ConcurrentHashMap: 其扩容过程更加精细且并发友好,通过多个线程同时参与扩容操作,进一步提高了效率

二、源码简析

1.并发控制机制

HashMap (简单无锁,非线程安全)

// HashMap的get方法非常简单,直接访问指定位置的桶
V get(Object key) {Node<K,V> e; // 用于存放找到的节点return (e = getNode(hash(key), key)) == null ? null : e.value;
}// 内部查找节点的方法
final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 首先检查第一个节点是否即为目标节点if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))return first;// 如果第一个节点不是,则沿着链表或树结构继续查找if ((e = first.next) != null) {// ...遍历逻辑省略,查找匹配的节点...}}return null;
}

注: HashMap的get操作没有内置的线程安全措施,任何并发修改都可能引起不可预测的行为。它假设在外部进行了同步处理,或在单线程环境下使用。

ConcurrentHashMap (高级并发控制)

    /*** 返回与指定键关联的值,如果此映射中不存在该键的映射,则返回{@code null}。** @param key 要查询的键* @return 与键关联的值,如果没有则为{@code null}*/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) {ek = e.key; // 保存键引用以供比较if ((ek == key) || (ek != null && key.equals(ek))) // 键相等return e.val; // 返回值}// 特殊处理:eh < 0 表示可能遇到了ForwardingNode(正在进行resize或其它结构调整)else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; // 在ForwardingNode中查找键// 未在首节点找到,遍历链表或树查找while ((e = e.next) != null) {if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {return e.val; // 找到匹配的键,返回其值}}}// 未找到匹配项,返回nullreturn null;}

​ 通过无锁读取提高并发读性能。它首先计算键的哈希值并定位到桶,然后直接访问桶的第一个节点,无需加锁。如果第一个节点匹配,则直接返回其值否则,它会遍历桶内的链表或树结构,查找匹配的节点。这种方法在高并发环境下能够显著提升读取性能,因为读操作不需要等待写锁,减少了线程间的竞争

    /*** 将指定的键与值关联到此映射表中。* 键和值都不能为null。** <p>可以通过使用与原始键相等的键调用{@code get}方法来检索该值。** @param key 要与此指定值关联的键* @param value 要与指定键关联的值* @return 与{@code key}先前关联的值,如果没有映射则为{@code null}* @throws NullPointerException 如果指定的键或值为null*/public V put(K key, V value) {return putVal(key, value, false);}/*** 实现put和putIfAbsent方法的内部逻辑。* * @param key 要插入的键* @param value 要插入的值* @param onlyIfAbsent 如果为true且键已存在,则不更新值* @return 键的旧值,如果没有则为null*/final V putVal(K key, V value, boolean onlyIfAbsent) {// 检查键和值是否为nullif (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) {// 使用CAS尝试将新节点放入空桶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) { // 锁定桶头节点或树节点// 再次检查桶状态,防止A-B-A问题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 方法实现 put 和 putIfAbsent 功能的核心逻辑,它负责在 ConcurrentHashMap 中插入或更新键值对。以下是该方法的主要行为和结论概述:

  • 键值验证:确保传入的键和值都不为 null,违反此规则将抛出 NullPointerException。

  • 哈希与索引计算:通过对键的哈希码应用扰动函数计算得到最终哈希值,并据此确定桶的索引。

  • 表初始化与扩容处理
    若表尚未初始化,通过 initTable 进行初始化。
    若当前桶正在被转移(因为表正在扩容),则帮助完成转移操作。

  • 无锁插入尝试:当目标桶为空时,尝试使用 CAS 操作直接插入新节点,以避免加锁开销。

  • 加锁与冲突处理
    对于非空桶,根据头节点类型(链表或树)加锁处理。
    遍历桶内链表或在树中搜索匹配的键。
    若键已存在,则根据 onlyIfAbsent 参数决定是否更新值。
    若键不存在,则插入新节点至链表尾部或树的适当位置。

  • 树化阈值检查:当桶内节点数量达到 TREEIFY_THRESHOLD 时,将链表转换为红黑树以优化性能。

  • 计数与维护:插入成功后,调用 addCount 方法更新元素数量,并可能触发进一步的扩容判断。

  • 返回结果:若键已存在,则返回旧值;否则,在某些情况下可能返回 null。

2.数据结构转换:链表转红黑树

  • HashMap
    当链表长度达到阈值时,HashMap会尝试将链表转换为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;// 检查是否需要先扩容if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();else if ((e = tab[index = (n - 1) & hash]) != null) {// 将链表转换为红黑树TreeNode<K,V> hd = null, tl = null;do {TreeNode<K,V> p = replacementTreeNode(e, null); // 转换节点类型if (tl == null)hd = p;else {p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);// 将红黑树根节点设置回桶中if ((tab[index] = hd) != null)hd.treeify(tab);}
}
  • ConcurrentHashMap
    /*** 将给定索引处桶中的所有链表节点转换为树形节点,除非表太小,* 在这种情况下会先进行扩容操作。*/private final void treeifyBin(Node<K,V>[] tab, int index) {Node<K,V> b; // 指向当前桶的第一个节点int n, sc; // 分别表示表的长度和树化过程中记录的节点数量// 确保表不为空if (tab != null) {// 检查表容量是否小于最小树化容量,若小于则先尝试扩容if ((n = tab.length) < MIN_TREEIFY_CAPACITY)tryPresize(n << 1); // 尝试将表容量加倍// 检查桶内有有效节点且不是正在转移的 forwarding nodeelse if ((b = tabAt(tab, index)) != null && b.hash >= 0) {synchronized (b) { // 对桶头节点加锁,保护转换过程// 再次检查桶头是否未改变,防止并发修改if (tabAt(tab, index) == b) {// 初始化树的头节点和尾节点TreeNode<K,V> hd = null, tl = null;// 遍历桶内链表,将每个节点转换为TreeNodefor (Node<K,V> e = b; e != null; e = e.next) {TreeNode<K,V> p =new TreeNode<K,V>(e.hash, e.key, e.val, null, null);// 构建TreeNode的双向链表结构if ((p.prev = tl) == null)hd = p; // 设置头节点elsetl.next = p;tl = p; // 移动尾节点指针}// 将桶设置为新的TreeBin,完成树化setTabAt(tab, index, new TreeBin<K,V>(hd));}}}}}

它负责在ConcurrentHashMap的桶中链表转换为红黑树结构,以提高在哈希冲突较多情况下的查找效率。此方法首先检查表容量是否满足树化的最小要求然后遍历桶内链表将每个节点转换为TreeNode对象,并构建这些树节点的双向链表结构,最后将桶的头节点替换为一个TreeBin实例,完成树化过程。在整个转换过程中,通过加锁保证了操作的线程安全性

static final class TreeBin<K,V> extends Node<K,V>内部类TreeBin

/*** 使用以节点 `b` 为首的初始节点集创建一个二叉树节点容器。* 此构造方法负责将一系列已排序的节点(通常源自链表)构造成红黑树结构。** @param b 初始节点集合的头节点,用于构建红黑树。*/
TreeBin(TreeNode<K,V> b) {// 调用父类构造器进行初始化,设置TREEBIN标记以及其它属性为null。super(TREEBIN, null, null, null);// 保存传入的头节点到first字段。this.first = b;// 初始化红黑树的根节点为null。TreeNode<K,V> r = null;// 遍历链表中的每个节点。for (TreeNode<K,V> x = b, next; x != null; x = next) {// 将当前节点的下一个节点暂存至next。next = (TreeNode<K,V>)x.next;// 将当前节点的左右子节点清空,准备构建红黑树。x.left = x.right = null;// 如果红黑树根节点尚未设定(首次遍历或新分支):if (r == null) {// 设定当前节点为根,无父节点,颜色设为黑色。x.parent = null;x.red = false;r = x; // 更新根节点为当前节点。} else {// 获取当前节点的键和哈希值,准备插入红黑树。K k = x.key;int h = x.hash;// 准备比较器类引用。Class<?> kc = null;// 在红黑树中查找当前节点的插入位置。for (TreeNode<K,V> p = r;;) {int dir, ph;K pk = p.key;// 根据键的哈希值比较确定插入方向。if ((ph = p.hash) > h)dir = -1;else if (ph < h)dir = 1;else { // 哈希值相同时,根据键的自然顺序或比较器决定。if (kc == null && (kc = comparableClassFor(k)) == null)// 键不可比较,则使用tie-break规则。dir = tieBreakOrder(k, pk);else// 可比较,则直接比较结果作为方向。dir = compareComparables(kc, k, pk);}// 记录当前父节点。TreeNode<K,V> xp = p;// 按照dir方向移动至下一个节点,若为空则插入当前节点。if ((p = (dir <= 0) ? p.left : p.right) == null) {x.parent = xp;if (dir <= 0)xp.left = x;elsexp.right = x;// 插入后平衡红黑树。r = balanceInsertion(r, x);break;}}}}// 设置构造完成的红黑树的根节点。this.root = r;// 断言检查红黑树的不变性条件是否满足。assert checkInvariants(root);
}
  • 接收一个头节点b遍历以其为首的节点集合,将这些节点逐个插入到一个新的红黑树结构中。过程中,代码不仅重新组织了节点间的链接关系(父母、左右子节点),还通过平衡插节点是黑色;任意节点到其每个叶子节点的所有路径上包含相同数量的黑色节点;且对于每个红色节点,其两个子节点都是黑色的)。通过这种方式,该构造函数保证了转换后的红黑树维持了良好的搜索性能特性,即最坏情况下的时间复杂度为O(log n)。
  • 遍历链表的过程中,代码首先清空每个节点的左右子节点信息,这是因为这些节点之前作为链表的一部分,没有树结构的概念。然后,通过比较节点的哈希值及键值(如果哈希值相同)来确定新节点在树中的插入位置,这一过程体现了红黑树的排序特性。当遇到相等哈希值的情况时,代码还会尝试利用键的自然顺序或者自定义比较器来进一步区分,确保键的唯一性和有序性。
  • 特别地,代码中还包括了一个平衡调整的过程,通过balanceInsertion方法在插入新节点后对树进行必要的旋转和重新着色操作,以维护红黑树的平衡状态。这是确保树操作效率的关键步骤,避免了因连续插入导致树形态退化成链式结构的可能性。
  • 最后,通过断言assert checkInvariants(root);来验证构建完成的红黑树是否满足红黑树的基本不变量,这是一种调试辅助手段,用于在开发和测试阶段捕捉可能的逻辑错误,确保数据结构的正确性。在生产环境中,断言的启用与否通常取决于JVM的启动参数配置。

3.扩容机制

触发hashMap和concurentHashMap扩容机制的条件

HashMap 扩容条件

  • 加载因子(Load Factor):HashMap 默认的加载因子为 0.75。这意味着当 HashMap 中的元素数量达到其容量的 75% 时,HashMap 会自动进行扩容操作。这是为了防止哈希冲突过于频繁,影响性能。
  • 容量(Capacity):扩容时,HashMap 会创建一个新的更大的数组,并将原有数组中的元素重新分配到新数组中。扩容后的容量通常是原容量的两倍(例如,从 16 扩展到 32)。

ConcurrentHashMap 扩容条件

  • 段(Segment)或桶(Bucket)级别扩容:在 Java 8 之前的版本中,ConcurrentHashMap 是由多个 Segment 组成的分段锁结构。每个 Segment 相当于一个小的 HashMap,有各自的扩容机制当某个 Segment 中的元素数量达到其阈值时,会单独对该 Segment 进行扩容。而在 Java 8 及以后版本中,虽然去除了 Segment,但扩容逻辑类似,关注的是桶(Bucket)级别的增长
  • 全局容量与负载:整体上,ConcurrentHashMap 也会监控其全局的元素数量与容量的比例,当总体元素数量达到全局阈值时,会触发整体的扩容操作。默认的加载因子也是 0.75,意味着当表中的元素数量达到总容量的 75% 时,会进行扩容
  • 并发控制:ConcurrentHashMap 在扩容时采用了细粒度的锁机制,确保扩容操作能与读写操作并发执行,减少阻塞。扩容时,不是一次性复制所有桶,而是逐步将桶从旧数组迁移到新数组,这个过程中仍然允许其他线程的读写操作

总的来说,HashMap 和 ConcurrentHashMap 都是在元素数量达到一定比例(默认为 75%)时触发扩容,但 ConcurrentHashMap 通过更精细的并发控制机制,在扩容的同时能保持较高的并发性能

HashMap

/*** 初始化或加倍哈希表容量。如果原表为空,则按照阈值字段保存的初始容量目标分配空间。* 否则,由于采用2的幂次扩展方式,来自每个桶的元素必须保持索引不变,* 或者在新表中以2的幂次偏移量移动。** @return 新的哈希表数组*/final Node<K,V>[] resize() {Node<K,V>[] oldTab = table; // 保存旧的哈希表引用int oldCap = (oldTab == null) ? 0 : oldTab.length; // 获取旧容量int oldThr = threshold; // 保存旧的扩容阈值int newCap, newThr = 0; // 初始化新容量和新阈值// 如果旧容量大于0if (oldCap > 0) {// 如果旧容量已达到最大,设置阈值为最大整数并返回原表if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// 否则,如果可以扩大两倍且旧容量至少为默认初始容量,则翻倍容量和阈值else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // 阈值也翻倍}// 如果旧阈值大于0,说明初始容量被放在了阈值字段里else if (oldThr > 0) newCap = oldThr;// 如果旧阈值为0,使用默认初始容量和加载因子计算新容量和新阈值else {               newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 如果新阈值未设置(仍为0),根据新容量和加载因子计算新阈值if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}// 设置新的扩容阈值threshold = newThr;// 创建新容量大小的新哈希表数组@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];// 将新表赋值给table引用table = newTab;// 如果旧表不为空,开始转移数据if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e; // 当前桶的头节点// 遍历桶中的节点if ((e = oldTab[j]) != null) {// 清除旧桶引用,准备重用节点oldTab[j] = null;// 单个节点直接迁移if (e.next == null)newTab[e.hash & (newCap - 1)] = e;// 如果是树节点,则执行特殊分裂操作else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);// 对于链表节点,维持顺序并重新分配到新桶else { // preserve orderNode<K,V> loHead = null, loTail = null; // 低位链表头尾指针Node<K,V> hiHead = null, hiTail = null; // 高位链表头尾指针Node<K,V> next;do {next = e.next;// 根据hash值确定节点应分配到新表的哪个部分if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 将处理好的链表分别放入新表对应位置if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;	 // 高位链表应放置在原索引加上旧容量的位置}}}}}return newTab;	// 返回扩容后的新哈希表}

ConcurrentHashMap

触发扩容的检查
addCount方法中,有一个环节用于检查是否需要扩容

private final void addCount(long x, int check) {CounterCell[] as; long b, s;if (check <= 1) {// 省略其他逻辑...}else if (as = counterCells != null ||!U.compareAndSwapLong(this, BASECOUNT, v = baseCount, s = v + x)) {// 省略其他逻辑...}// 检查是否需要扩容if (check >= 0) {Node<K,V>[] tab, nt; int n, sc;while ((tab = table) != null &&(n = tab.length) < MAXIMUM_CAPACITY &&(sc = sizeCtl) >= 0) {if (sc < n << RESIZE_STAMP_SHIFT) {if ((sc >>> RESIZE_STAMP_SHIFT) != resizeStamp(n) || sc == resizeStamp(n) + 1 ||sc == resizeStamp(n) + MAX_RESIZERS || transferIndex <= 0)break;if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))transfer(tab, null);}// 其他情况省略...}}
}
/*** 负责在扩容时将原哈希表中的节点重新分配到新的、更大的哈希表中。* 实现并发安全的节点迁移,以支持HashMap在使用过程中的动态扩容。** @param tab 当前正在使用的哈希表数组* @param nextTab 新的、扩容后的哈希表数组*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {int n = tab.length; // 获取当前哈希表的长度int 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;}// 准备进入完成阶段,减少sizeCtl并重置iif (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) { // 处理链表桶// 初始化runBit为当前桶头节点的哈希值与原哈希表长度-1的按位与结果int runBit = fh & n;// 初始化lastRun为当前桶的第一个节点,即f自身Node<K,V> lastRun = f;// 遍历当前桶中的链表for (Node<K,V> p = f.next; p != null; p = p.next) {// 计算当前节点p的哈希值与原哈希表长度-1的按位与结果int b = p.hash & n;// 检查当前节点的runBit是否与之前的不同if (b != runBit) {// 如果不同,说明遇到了需要划分到不同桶的节点// 更新runBit为当前节点的b值,并记录下这个作为新runBit的第一个节点lastRunrunBit = b;lastRun = p;}}// 根据最后的runBit决定高低位链表的分配// 如果runBit为0,说明从f到lastRun这一段应该分配到新表的低位桶(索引不变)if (runBit == 0) {ln = lastRun; // ln链表指向lastRun,包含从f到lastRun的节点hn = null;    // hn链表为空,因为所有处理的节点都属于低位}// 否则,runBit不为0,这一段应该分配到新表的高位桶(索引加上原表长度)else {hn = lastRun; // hn链表指向lastRun,包含从f到lastRun的节点ln = null;    // ln链表为空,因为所有处理的节点都属于高位}// 遍历当前桶中的链表,根据节点的hash值分配到ln(低位链表)或hn(高位链表)for (Node<K,V> p = f; p != null; p = p.next) {int ph = p.hash;K pk = p.key;V pv = p.val;Node<K,V> newNode = new Node<>(ph, pk, pv, null);if ((ph & n) == 0) {if (ln == null)ln = newNode;elseln.next = newNode;} else {if (hn == null)hn = newNode;elsehn.next = newNode;}}// 将拆分后的链表放入新哈希表的对应位置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;// 遍历树节点,根据hash值分配到lo(低位树)或hi(高位树)for (Node<K,V> e = t.first; e != null; e = e.next) {TreeNode<K,V> p = new TreeNode<>(e.hash, e.key, e.val, null, null);if ((p.hash & n) == 0) {if (loTail == null)lo = p;elseloTail.next = p;loTail = p;lc++;} else {if (hiTail == null)hi = p;elsehiTail.next = p;hiTail = p;hc++;}}// 根据拆分结果构建新树或链表ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (hc != 0) ? new TreeBin<>(lo) : t;hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (lc != 0) ? new TreeBin<>(hi) : t;// 将处理后的树或链表放入新哈希表setTabAt(nextTab, i, ln);setTabAt(nextTab, i + n, hn);setTabAt(tab, i, fwd);advance = true; // 标记可以处理下一个桶}}} // synchronized 结束} // if (fh >= 0) 或 (f instanceof TreeBin) 结束} // else (桶非空且未转移) 结束} // 主循环结束
} // transfer 方法结束

transfer方法是Java ConcurrentHashMap类中的核心方法之一,其主要职责是在哈希表扩容时,将原哈希表中的所有节点(包括链表节点和树节点)高效且安全地迁移到一个新的、容量更大的哈希表中。以下是该方法的关键点总结:

1)并发转移: 利用多线程并发执行,每个处理器负责哈希表的一部分桶进行迁移,提高了扩容效率,同时确保操作的线程安全性。
2)步长计算: 动态计算每个线程的处理步长(stride),平衡并发度与竞争,特别是在CPU核心数较多时,合理分配每个线程的工作量。
3)新表初始化: 在第一次调用时,会创建一个两倍于原表大小的新哈希表,并处理可能的OutOfMemoryError异常。
4)桶的遍历与转移:

  • 使用transferIndex和bound来控制遍历范围,逐步推进,确保所有桶都被处理。
  • 对于空桶,直接设置转发节点。
  • 对于已标记为“MOVED”的桶,跳过处理(已被其他线程处理)。
  • 对于链表桶,根据节点的哈希值将其分割为两个链表,分别放置在新表的原索引和原索引+n的位置。
  • 对于树形桶(当链表长度超过阈值时转换而成),同样进行拆分处理,可能重新调整为链表或新的树形结构。

5)控制转移流程:

  • 使用finishing标志来确保所有桶至少被检查过一次,之后进入最终的整理阶段,提交新表并更新控制状态。
  • 通过CAS操作保证操作的原子性和一致性,减少线程间的直接竞争。

6)桶内同步: 在处理非空桶时,通过同步块锁定桶头节点,确保同一时间只有一个线程能操作该桶,保证了在并发环境下的数据一致性。
7)树形节点的处理: 特别处理了树形桶的拆分,将树拆分为两个部分,可能重新转换为链表或新的树结构,确保数据结构的优化。

总之,transfer方法通过精细的并发控制和数据结构操作,实现了高效且线程安全的哈希表扩容过程,是ConcurrentHashMap高并发性能的关键实现之一。

三、putIfAbsent方法computeIfAbsent方法区别

场景代码

    public static void main(String[] args) {test(new HashMap<>());test(new ConcurrentHashMap<>());}private static void test(Map<String, String> map) {log.info("class : {}", map.getClass().getName());try {log.info("putIfAbsent test1 null value : {}", map.putIfAbsent("test1", null));} catch (Exception ex) {ex.printStackTrace();}log.info("test containsKey test1 after putIfAbsent : {}", map.containsKey("test1"));log.info("computeIfAbsent test2 null value : {}", map.computeIfAbsent("test2", k -> "20"));log.info("test containsKey test2 after computeIfAbsent : {}", map.containsKey("test2"));log.info("putIfAbsent test3 non-null value : {}", map.putIfAbsent("test3", "test3"));log.info("computeIfAbsent test4 non-null value : {}", map.computeIfAbsent("test4", k -> "test4"));log.info("putIfAbsent test4 expensive value : {}", map.putIfAbsent("test4", getValue()));log.info("computeIfAbsent test4 expensive value : {}", map.computeIfAbsent("test4", k -> getValue()));}private static String getValue() {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}return UUID.randomUUID().toString();}

传入HashMap结果

在这里插入图片描述

传入ConcurrentHashMap结果:

在这里插入图片描述

结论:

前提:HashMap只可以有一个K=null,V随意;ConcurrentHashMap的K-V 都!=null

  • putIfAbsent:当K-V不存在,将待插入的K-V插入,返回null;当K-V存在,返回当前K-V的值,有函数也不更新;
  • computeIfAbsent:当K-V不存在,将对应K的计算结果插入(但是当V=null,则不插入K-null);当K-V存在,返回原映射中的值;

看完笔者这篇文章余兴不足,还可以看看别人的

HashMap和ConcurrentHashMap的区别,详解HashMap和ConcurrentHashMap数据结构

HashMap和ConcurrentHashMap

HashMap和ConcurrentHashMap详细笔记

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/860360.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux(简单概述)

目录 第一章 初识Linux 第四章 文件管理与常用命令 1.文件基础知识 2.文件显示命令 3.文件内容查询 4. 文件和目录基本操作 5. 文件复制、移动、删除 7. 链接 8. 文件访问权限 9. 文件查找命令 10. 压缩和解压缩 第五章用户与用户组 第六章软件包管理RPM和YUM数据库…

面试-synchronized(java5以前唯一)和ReentrantLock的区别

1.ReentrantLock&#xff08;再入锁&#xff09;&#xff1a; (1).在java.util.concurrent.locks包 (2).和CountDownLatch,FutureTask,Semaphore一样基于AQS实现。 AQS:AbstractQueuedSynchronizer 队列同步器。Java并发用来构建锁或其他同步主键的基础框架&#xff0c;是j.u.c…

如何使用命令提示符查询电脑相关序列号等信息的操作方法

如何使用命令提示符查询硬盘的序列号&#xff1f; 如果出于保修或其他目的&#xff0c;你想知道硬盘驱动器的序列号&#xff0c;你不想使用第三方应用程序&#xff0c;或者如果你更喜欢命令行方法&#xff0c;则可以使用带有命令提示符的命令来显示硬盘驱动器的序列号。 1. 按…

【SQL Server数据库】数据的增删改操作

目录 一、用SQL语句完成下列功能。 1、新开设一门课程&#xff0c;名叫网络安全与防火墙&#xff0c;学时40&#xff0c;编号为“0118”&#xff0c;主要介绍网络的安全与主要的防火墙软件。 2、先建立monitor表&#xff0c;其结构与student表大致一样&#xff0e;…

图书管理系统(详解版 附源码)

目录 项目分析 实现页面 功能描述 页面预览 准备工作 数据准备 创建数据库 用户表 创建项目 导入前端页面 测试前端页面 后端代码实现 项目公共模块 实体类 公共层 统一结果返回 统一异常处理 业务实现 持久层 用户登录 用户注册 密码加密验证 添加图书…

Cesium默认bing地图数据,还支持哪些地图的数据源呢?

传统的前端开发增长乏力了&#xff0c;新兴的web3D方向前端开发需求旺盛&#xff0c;这一块在国外很成熟&#xff0c;在国内兴起不久&#xff0c; 甚至很多前端老铁都没听过&#xff0c;没见过&#xff0c;没有意识到&#xff0c;前端除了框架、vue、uniapp这些烂大街的&#x…

黑马苍穹外卖7 用户下单+订单支付(微信小程序支付流程图)

地址簿 数据库表设计 就是基本增删改查&#xff0c;与前面的类似。 用户下单 用户点餐业务流程&#xff1a; 购物车-订单提交-订单支付-下单成功 展示购物车数据&#xff0c;不需要提交到后端 数据库设计&#xff1a;两个表【订单表orders&#xff0c;订单明细表order_d…

cnpm run dev 报错 Error: Cannot find module ‘fs/promises’

主要原因是babel版本冲突 卸载以下依赖可以解决问题&#xff1a; 之后重新安装babel-loader依赖 可能会报以下错误&#xff1a; 接着安装babel-core依赖 项目顺利启动

【启明智显分享】低成本RISC-V工业级HMI方案推荐

伴随着工业4.0的迅猛发展&#xff0c;工业HMI以方便、快捷的特点逐渐成为工业的日常应用&#xff0c;成为备受追捧的全新多媒体交互设备。 什么是工业HMI&#xff1f;工业HMI是用于工业自动化系统中的人机交互界面&#xff0c;通常由触摸屏、按钮、指示灯、显示器等组成&#…

如何正确使用C#短信接口发送招生短信

群发短信对教育机构来讲虽然是个不错的招生工具,但怎么使用决定着生源转化效率,如果是为了单纯的发短信而发短信效率当然不好,那么如何正确使用招生群发短信呢?技巧才是关键! 教育短信发送较多的就是招生群发短信内容,而运营商对教育行业内容审核一般比较严格,需要短信公司特殊…

新媒体矩阵系统是什么?怎么搭建矩阵系统?

目录 前言&#xff1a; 一、新媒体矩阵分别是什么&#xff1f; 1、横向矩阵 2、 纵向矩阵 二、新媒体矩阵的作用&#xff1f; 1、多元化发展&#xff0c;吸引目标 2、多平台协同&#xff0c;放大宣传效果 3、多平台运营&#xff0c;分散风险 三、怎么做矩阵系统&…

【摄像头标定】双目摄像头标定及矫正-opencv(python)

双目摄像头标定及矫正 棋盘格标定板标定矫正 棋盘格标定板 本文使用棋盘格标定板&#xff0c;可以到这篇博客中下载&#xff1a;https://blog.csdn.net/qq_39330520/article/details/107864568 标定 要进行标定首先需要双目拍的棋盘格图片&#xff0c;20张左右&#xff0c;…

企业微信内嵌H5项目接入聊天功能

产品需求是,在列表中把符合条件的列表接入聊天功能,以下是详细步骤: 1.引入企业微信 <script src"https://res.wx.qq.com/wwopen/js/jsapi/jweixin-1.0.0.js"></script> 2.获取wx签名(必须要) /*** 获取wx签名**/ export function getWxJsApi(data) {r…

pdf压缩,pdf压缩在线,pdf文件太大怎么变小

在数字化时代&#xff0c;PDF文档因其跨平台、保持原样、易于阅读和打印等特点&#xff0c;成为了我们日常工作和生活中不可或缺的一部分。然而&#xff0c;随着PDF文件的不断累积&#xff0c;存储空间逐渐变得紧张&#xff0c;特别是在处理大量大型PDF文件时&#xff0c;如何有…

若依前后端分离项目整合shardingjdbc分表(详细,分片字段订单id)

文章目录 1. 引入Maven依赖2.引入配置文件3.兼容之前的数据库源,使用现在的sharding数据库源&#xff08;shardingjdbc默认的数据源&#xff09;&#xff0c;但是配置好文件之后是没有生效的&#xff0c;需要加配置文件覆盖4. 检测是否成功5. 如何使用&#xff0c;在需要使用的…

qt开发-14_QListwidget 仿qq好友列表制作

QListWidget 继承 QListView。QListWidget 类提供了一个基于项的列表小部件。QListWidg et 是一个便捷的类&#xff0c;它提供了一个类似于 QListView&#xff08;下一小节将讲到&#xff09;提供的列表视图&#xff0c;但 是提供了一个用于添加和删除项目的基于项目的经典接口…

第10章 启动过程组 (启动过程组的重点工作)

第10章 启动过程组 10.3启动过程组的重点工作&#xff0c;在第三版教材第362~364页&#xff1b; 文字图片音频方式 第一个知识点&#xff1a;项目启动会议 1、作用 标志着对项目经理责权的定义结果的正式公布&#xff0c;通常由项目经理负责组织和召开。2、目的 使项目各…

分享:Wordpress插件-AI Image Pro v2.6.0中文版语言包

AI Image Pro是一个wordpress插件,可将您的 WordPress 网站直接与最佳图像生成 A模型(DALL.E和稳定扩散)集成&#xff0c;并允许您利用 AI 生成图像、编辑(内画)图像和创建图像变体。最重要的是&#xff0c;它配备了许多一键式滤镜和许多微调选项,您可以使用它们来增强和创建令…

黑匣子问题:大语言模型的内部工作原理

像 GPT-3 这样的大型语言模型 (LLM) 已经展示了令人印象深刻的自然语言能力&#xff0c;但它们的内部工作原理仍然知之甚少。这种“黑匣子”性质使得ChatGPT在敏感的现实应用程序中部署时可能会出现问题。 什么是LLM黑匣子问题&#xff1f; 语言学习模型 (LLM) 是强大的工具&…

要求全国70%中医院设置康复科!康复科门诊服务这样建设!

近日&#xff0c;国家中医药管理局印发《国家中医药管理局关于进一步加强中医医院康复科建设的通知》提出&#xff0c;中医医院应根据当地人口规模及中医药康复服务需求设置康复科并作为独立科室进行建设和管理。有条件的二级以上中医医院应当按照《中医医院康复科推荐配置标准…