Map学习笔记——深入理解ConcurrentHashMap

news/2024/5/9 6:50:49/文章来源:https://blog.csdn.net/BASK2311/article/details/128061881

ConcurrentHashMap 是我们日常开发中使用频率最高的并发容器之一了,具有如下特点:

基于JDK8分析

  1. 存储结构和HashMap一样,都是数组 + 链表 + 红黑树
  2. 线程安全的容器,底层是通过CAS自旋 + sychronized 来保证的
  3. key 和 value 都不允许为空,否则将抛出空指针异常
  4. 对比HashTalbe,锁的粒度控制在数组的桶元素上
  5. 可以多线程协助扩容

接下来从源码的角度来分析下 ConcurrentHashMap

1. 关键属性

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>implements ConcurrentMap<K,V>, Serializable {private static final long serialVersionUID = 7249069246763182397L;/*** 数组的最大长度 (和HashMap一样)*/private static final int MAXIMUM_CAPACITY = 1 << 30;/***  数组的默认长度 (和HashMap一样)*/private static final int DEFAULT_CAPACITY = 16;/*** 数组中的元素个数最大值,超过这个阈值将抛出OOM*/static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;/*** The default concurrency level for this table. Unused but defined for* compatibility with previous versions of this class.** 翻译过来是默认的并发级别,但是并没有使用*/private static final int DEFAULT_CONCURRENCY_LEVEL = 16;/*** 加载因子*/private static final float LOAD_FACTOR = 0.75f;/*** 当链表长度达到8,转成红黑树*/static final int TREEIFY_THRESHOLD = 8;/*** 当红黑树的长度小于6,转成链表*/static final int UNTREEIFY_THRESHOLD = 6;/*** 当链表长度大于8,但是数组table长度没有达到64,此时不会转成红黑树,而是扩容*/static final int MIN_TREEIFY_CAPACITY = 64;/*** 最小的迁移步长,当多线程并发扩容时,每个线程负责的迁移的最小桶的个数*/private static final int MIN_TRANSFER_STRIDE = 16;/*** The number of bits used for generation stamp in sizeCtl.* Must be at least 6 for 32bit arrays.*/private static int RESIZE_STAMP_BITS = 16;/*** The maximum number of threads that can help resize.* Must fit in 32 - RESIZE_STAMP_BITS bits.* 表示并发扩容最大线程数*/private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;/*** The bit shift for recording size stamp in sizeCtl.* 和扩容相关,用来标识sizeCtl的符号位*/private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;/** Encodings for Node hash fields. See above for explanation.*///旧数组迁移后,桶位会放入ForwardingNode节点,hash值固定为-1static final int MOVED     = -1; // hash for forwarding nodes//红黑树在桶中元素的节点(TreeBin)的hash值固定为-2static final int TREEBIN   = -2; // hash for roots of treesstatic final int RESERVED  = -3; // hash for transient reservations//保证hash值都是正数static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash/** 处理器的核数*/static final int NCPU = Runtime.getRuntime().availableProcessors();/*** table*/static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;volatile V val;            //value是volatile的volatile Node<K,V> next;   //next是volatile的Node(int hash, K key, V val, Node<K,V> next) {this.hash = hash;this.key = key;this.val = val;this.next = next;}//.......}/* ---------------- Fields -------------- *//*** table数组,长度总是2的N次幂*/transient volatile Node<K,V>[] table;/*** 扩容时使用的*/private transient volatile Node<K,V>[] nextTable;/*** 记录元素个数的基准值*/private transient volatile long baseCount;/*** table数组初始化和扩容的控制符,不同值有不同的含义:* 1.如果值为-1,则表示table数组正在初始化;-(n + 1) 表示表示此时有n个线程正在共同完成数组的扩容操作* 2.如果值为0,表示table数组还没有被初始化,默认长度是16* 3.值为正数,如果数组没有初始化,则记录的是数组的初始容量;如果数组已经初始化,记录的是数组的扩容阈值(capacity * loadFactor)** 这个值是非常重要的*/private transient volatile int sizeCtl;/*** 迁移时的索引*/private transient volatile int transferIndex;/*** 自旋锁标志位* 在在调整CounterCell[]大小和或创建CounterCell[]时使用。*/private transient volatile int cellsBusy;/*** 当put元素并进行计数时,优先对baseCount进行加1操作,但是并发度高的情况下,只有一个线程能* 成功,其他失败的线程将会使用CounterCell进行统计。* 这样元素的总个数 = baseCount + CounterCell[]数组各个下标对应值的总和*/private transient volatile CounterCell[] counterCells;}
复制代码

sizeCtl 属性着重说明一下:

  1. 如果值为 -1,则表示table数组正在初始化;-(n + 1) 表示表示此时有多个线程正在共同完成数组的扩容操作
  2. 如果值为0,表示table数组还没有被初始化,默认长度是16
  3. 值为正数,如果数组没有初始化,则记录的是数组的初始容量;如果数组已经初始化(table != null),记录的是数组的扩容阈值(capacity * loadFactor)

2.构造器分析

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>implements ConcurrentMap<K,V>, Serializable {private static final long serialVersionUID = 7249069246763182397L;/*** 如果使用的空参数构造器,那么默认的长度16*/public ConcurrentHashMap() {}/*** 指定初始容量* 如果初始容量大于(最大值/2 = 2^29),那么初始容量就是默认的最大值2^30* 否则去初始容量的(1.5倍+1),然后计算出大于该值的最小的2的N次幂*** 同时,将初始容量赋给sizeCtl, 表示数组还没有初始化*/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;}//.....其他构造器......}
复制代码

tableSizeFor()方法并不陌生,前面讲HashMap的时候,我们有提到,不过二者在计算数组的初始容量稍有差异,HashMap直接对指定容量进行位运算,计算出大于等于指定容量的最小的2的N次幂;而ConcurrentHashMap是先对指定容量进行(1.5倍 + 1), 然后对计算后的值进行位运算,得到大于该值的最小的2的N次幂。

HashMap: 7 ——> 8,17 ——> 32,16 ——> 16,1 ——> 1
ConcurrentHashMap: 7 ——> 16,17 ——> 32,32 ——> 64,1 ——> 2

3.put()方法分析

测试数据:

public class Test {public static void main(String[] args) {//此时初始容量是64,由于内部的table还没有初始化,所以sizeCtl属性值为初始容量64ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(32);map.put("abc", 12);}
}    
复制代码

源码分析:

final V putVal(K key, V value, boolean onlyIfAbsent) {//key和value 都不允许为空if (key == null || value == null) throw new NullPointerException();/*** 通过扰动函数,计算出hash值,高16位也参与运算,这个和HashMap一样* 不过计算出结果后,它会 &0x7fffffff,其目的是,这样计算出来的hash值都是正数!*/int hash = spread(key.hashCode());int binCount = 0;//死循环,搭配CAS做自旋用的for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;//如果数组是空的,则需要初始化table数组 ①if (tab == null || (n = tab.length) == 0)tab = initTable();//数组不为空,计算当前key在数组中桶的下标,如果为空,则表示没有放元素,则通过cas设置元素    else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break;                   //设置成功,break}//如果桶中元素的hash值为-1,表示当前数组正在扩容else if ((fh = f.hash) == MOVED)//当前线程协助扩容tab = helpTransfer(tab, f);else {  //否则就需要插入到链表或者红黑树中V oldVal = null;//对数组中的桶节点元素加锁synchronized (f) {//double check, 避免其他线程将树转成了红黑树,或者其他线程移除了该元素if (tabAt(tab, i) == f) {/*** fh >= 0, 表示普通的链表节点* 能走到这里,说明数组中的桶元素不为空,同时hash值也不是-1* 同时,如果是红黑树,则桶中元素放到的是TreeBin节点,它的hash值固定是-2,* 所以,如果 fh>=0,则是普通链表*/if (fh >= 0) {binCount = 1;//整个for循环是处理链表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) {//如果链表长度大于等于8,则将链表转成红黑树if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}//元素个数+1addCount(1L, binCount);return null;
}
复制代码

接下来我们研究一下功能点的细节方法

3.1 表的初始化——initTable()

private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;//while循环+CAS形成自旋while ((tab = table) == null || tab.length == 0) {/** 1.如果值为-1,则表示table数组正在初始化;-(n + 1) 表示表示此时有n个线程正在共同完成数组的扩容操作* 2.如果值为0,表示table数组还没有被初始化,默认长度是16* 3.值为正数,如果数组没有初始化,则记录的是数组的初始容量;如果数组已经初始化,记录的是数组的扩容阈值(capacity * loadFactor)**///如果值小于0,说明正在初始化或者扩容,此时当前的线程就直接放弃CPU的使用权//由运行状态变为就绪状态,再次获得CPU的控制权后接着往下执行,这里也就是继续执行while循环if ((sc = sizeCtl) < 0)Thread.yield(); // lost initialization race; just spin/*** 在测试数据中,我们指定了初始容量,在没有初始化的情况下,sizeCtl等于初始容量也就是64 > 0* 利用CAS将sizeCtl修改为-1** U.compareAndSwapInt(this, SIZECTL, sc, -1) * 如果SIZECTL(就是sizeCtl在内存中的偏移量)和sc相等,则将SIZECTL(就是sizeCtl)修改为-1*/else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {try {/** 为什么要double check ?* 因为finally 中 sizeCtl = sc; 此时 sizeCtl是扩容阈值,其他线程* 来到else if 判断为true, 则也可以进来,这样就重复初始化了*/if ((tab = table) == null || tab.length == 0) {//这里的sc还是初始容量,请看指定容量的构造器方法int n = (sc > 0) ? sc : DEFAULT_CAPACITY;@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = tab = nt;/*** 重新计算sc: 因为table已经初始化了,所以这sc值在这里就是扩容阈值了**  n - (n >>> 2) = n - n/4 = 3/4(n) = 0.75 * n*  sc = 0.75 * table.length*/sc = n - (n >>> 2);}} finally {sizeCtl = sc;}break;}}return tab;
}
复制代码

初始化完成之后,就不走其他判断了,直接重新循环,此时table已经不为空了,然后根据key计算在数组中的索引,判断是否为空,如果为空,则通过CAS插入元素。

3.2 CAS插入元素

这里比较简单,如果数组中该索引位置没有元素,则直接通过CAS插入即可;如果插入失败,则说明有其他线程先一步插入了该位置,则CAS失败,此时就重新for循环然后插入到链表位置。

接下来我们先看下如果出现hash冲突如何解决,协助扩容晚点再探究。

3.3 hash冲突

final V putVal(K key, V value, boolean onlyIfAbsent) {//key和value 都不允许为空if (key == null || value == null) throw new NullPointerException();/*** 通过扰动函数,计算出hash值,高16位也参与运算,这个和HashMap一样* 不过计算出结果后,它会 &0x7fffffff,其目的是,这样计算出来的hash值都是正数!*/int hash = spread(key.hashCode());int binCount = 0;//死循环,搭配CAS做自旋用的for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;//....省略其他else if........//hash碰撞的处理逻辑else {V oldVal = null;//对数组中的桶节点元素加锁synchronized (f) {//double check, 避免其他线程将树转成了红黑树,或者其他线程移除了该元素if (tabAt(tab, i) == f) {/*** fh >= 0, 表示普通的链表节点* 能走到这里,说明数组中的桶元素不为空,同时hash值也不是-1* 同时,如果是红黑树,则桶中元素放到的是TreeBin节点,它的hash值固定是-2,* 所以,如果 fh>=0,则是普通链表*/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) {//如果链表长度大于等于8,则将链表转成红黑树if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}//元素个数+1addCount(1L, binCount);return null;
}
复制代码

不难发现,如果出现hash冲突,则锁住当前桶位,对比HashTable, 锁的粒度直接从锁整个表下降到锁表中的某一个桶位,并发度将大大提高。如果是链表,则采用尾插法插入到链表尾部;如果是红黑树,则将元素插入到红黑树中;如果链表长度 >=8,则将链表转成红黑树。接下来我们简单看下链表转成红黑树的逻辑:

private final void treeifyBin(Node<K,V>[] tab, int index) {Node<K,V> b; int n, sc;if (tab != null) {/*** 链表长度大于8,但是数组长度没有达到64,此时不会转成红黑树,而是先扩容* 这一点和HashMap一样*/if ((n = tab.length) < MIN_TREEIFY_CAPACITY)tryPresize(n << 1); //扩容2倍,扩容的逻辑放到后面再说/*** 1.找到桶中的元素,并且不为空* 2.hash值要大于0,为什么?*   因为桶中元素的hash值小于0,只有两种情况*   1)数组正在扩容,旧数组迁移后的桶的位置会放一个 ForwardingNode 节点,它的hash值是-1*   2)桶中的元素已经是树了,此时桶中元素的hash值是-2*/else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {//锁住当前桶中的元素,开始树化synchronized (b) {/*** double check* 为什么要double check ?* 因为有可能当前线程进来了else if, 但是还没有执行sychronized获得锁之前,有其他线程* 已经获得了这个锁,然后开始树化,树化后节点元素可能随着左旋和右旋而改变,所以这里必须要double check*/if (tabAt(tab, index) == b) {//整个for循环就做一件事,将当前的单向链表转成双向链表//hd 是双向链表的头节点TreeNode<K,V> hd = null, tl = null;for (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);if ((p.prev = tl) == null)hd = p;elsetl.next = p;tl = p;}/*** 将当前桶的位置设置为 TreeBin 节点,然后将双向链表通过构造器传入到* 内部,然后再内部将双向链表转成红黑树。* 注意:桶中的TreeBin 的hash值是 -2*/setTabAt(tab, index, new TreeBin<K,V>(hd));}}}}
}
复制代码

我们用一张图来展示下红黑树的存储结构:

3.4 统计元素个数

我们看下addCount()方法的内部实现:

这个方法内部主要做两件事:一个是统计元素个数,另一个是扩容

private final void addCount(long x, int check) {CounterCell[] as; long b, s;/*** 1.统计元素个数* 优先对baseCount进行加,但是高并发下,只能有一个线程通过CAS添加成功,其他的线程CAS会失败* 此时那些失败的线程就会跳过对baseCount的运算,而是去对CounterCell[]数组中的元素进行计数* 这样总的元素个数 = baseCount + CounterCell[]数组各个元素的和*/if ((as = counterCells) != null ||!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {CounterCell a; long v; int m;boolean uncontended = true;if (as == null || (m = as.length - 1) < 0 ||(a = as[ThreadLocalRandom.getProbe() & m]) == null ||!(uncontended =U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {fullAddCount(x, uncontended);return;}if (check <= 1)return;s = sumCount();}/*** 如果check >= 0, 则需要判断是否扩容,因为remove()的话,也会调用调用这个方法* 传入的是 -1,此时是不需要扩容的。**/if (check >= 0) {Node<K,V>[] tab, nt; int n, sc;while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&(n = tab.length) < MAXIMUM_CAPACITY) {int rs = resizeStamp(n);if (sc < 0) {if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||transferIndex <= 0)break;if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))transfer(tab, nt);}else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2))transfer(tab, null);s = sumCount();}}
}
复制代码

我们先用一张图来整体概括下计数的逻辑:

我们来详细分析下统计元素个数的逻辑:

参考JUC下的原子类 LongAdder,他们的逻辑是一模一样的。使用了分段CAS的思想

/*** 1.统计元素个数* 优先对baseCount进行加,但是高并发下,只能有一个线程通过CAS添加成功,其他的线程CAS会失败* 此时那些失败的线程就会跳过对baseCount的运算,而是去对CounterCell[]数组中的元素进行计数* 这样总的元素个数 = baseCount + CounterCell[]数组各个元素的和*/
private final void addCount(long x, int check) {CounterCell[] as; long b, s;/*** 不难发现,一旦并发比较高,对baseCount加失败后,就会对counterCells计数,之后就都会* 对 counterCells 计数,不在对baseCount计数了。** 一旦CAS对baseCount计数成功,就退出了* 一旦CAS对baseCount计数失败,取反就是true,开始初始化counterCells然后计数* */if ((as = counterCells) != null ||!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {CounterCell a; long v; int m;boolean uncontended = true;if (as == null || (m = as.length - 1) < 0 ||(a = as[ThreadLocalRandom.getProbe() & m]) == null ||!(uncontended =U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {//对counterCells计数的核心逻辑  fullAddCount(x, uncontended);return;}if (check <= 1)return;s = sumCount();}//......先省略扩容的逻辑........
}    
复制代码

接下来我们看下对 counterCells 计数的逻辑:

核心逻辑主要就是集中在这3个判断中:

  1. 第一个判断主要是对 counterCells 数组进行扩容,每次也是以2的N次幂进行扩容,初始容量是2
  2. 第二个判断主要是初始化 counterCells 数组
  3. 第三个判断是如果对counterCells计数失败了,回过头来在对baseCount计数,吃回头草。

接下来我们在详细展开看看:

private final void fullAddCount(long x, boolean wasUncontended) {int h;/*** h: 可以理解为一个随机数,用来计数counterCells数组索引位置的* h = ThreadLocalRandom.getProbe()) == 0 说明还没有初始化,调用localInit()初始化,产生一个随机数*/if ((h = ThreadLocalRandom.getProbe()) == 0) {ThreadLocalRandom.localInit();      // force initializationh = ThreadLocalRandom.getProbe();wasUncontended = true;}//碰撞的意思boolean collide = false;                // True if last slot nonempty(如果最后一个槽位非空则为True)for (;;) {CounterCell[] as; CounterCell a; int n; long v;/*** 1.这里已经完成了对counterCells的初始化操作。我们先看步骤2判断**/if ((as = counterCells) != null && (n = as.length) > 0) {/*** 1.1计算counterCells数组中元素的位置是否为空** 无论h是什么,假如n就是初始容量2,h&1位运算结果只能是0或1(因为h只有最后一个位参与运算)* (a = as[(n - 1) & h]) == null 说明counterCells数组的位置没有值**/if ((a = as[(n - 1) & h]) == null) {//cellsBusy == 0 说明counterCells没有初始化也没有扩容if (cellsBusy == 0) { //创建一个  CounterCell 对象,将保存到 counterCells数组中CounterCell r = new CounterCell(x); // Optimistic create/*** 这里为什么还要对cellsBusy CAS 判断呢?* 说白了,来到这里也有可能是多个线程,所以必须确保只能有一个操作成功*/if (cellsBusy == 0 &&U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {boolean created = false;try {               //依然double check, 因为有可能执行完finally逻辑后,另一个线程就进来了CounterCell[] rs; int m, j;if ((rs = counterCells) != null &&(m = rs.length) > 0 &&rs[j = (m - 1) & h] == null) {rs[j] = r;created = true;}} finally {cellsBusy = 0;}if (created)break;continue;           // Slot is now non-empty}}collide = false;}//Uncontended 是无竞争的意思else if (!wasUncontended)       // CAS already known to failwasUncontended = true;      // Continue after rehash/*** 尝试用CAS对该单元格的值累加,如果成功则退出*/else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))break;else if (counterCells != as || n >= NCPU)collide = false;            // At max size or staleelse if (!collide)collide = true;/*** 多次尝试操作后,依然失败了,说明对couterCells并发非常高,则此时尝试扩容* 扩容为原来的2倍(也是2的N次幂)*/else if (cellsBusy == 0 &&U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {try {if (counterCells == as) {// Expand table unless staleCounterCell[] rs = new CounterCell[n << 1];for (int i = 0; i < n; ++i)rs[i] = as[i];counterCells = rs;}} finally {cellsBusy = 0;}collide = false;continue;                   // Retry with expanded table}//产生一个新的随机数h = ThreadLocalRandom.advanceProbe(h);}/*** 2.初始化counterCells数组,先看这里。* cellsBusy == 0 表示不忙,也就是还没有初始化,然后通过CAS将其修改为1,表示忙* 在搭配double check 这样其他线程就无法再进来重复初始化了**/else if (cellsBusy == 0 && counterCells == as &&U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {boolean init = false;try {  // double checkif (counterCells == as) {//创建一个长度为2的数组CounterCell[] rs = new CounterCell[2];/*** 计算索引位置,h是一个随机数* h & 1 的结果要么是0,要么是1,因为无论h是什么,它只有最后1位参与位运算* 索引,这个x值可能放在index = 0的位置,或者放到index = 1的位置*/rs[h & 1] = new CounterCell(x);//赋值,完成初始化操作counterCells = rs;init = true;}} finally {cellsBusy = 0;}if (init)break;}//如果对counterCells计数失败,回过头来在对baseCount计数else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))break;                          // Fall back on using base}
}
复制代码

整个流程我们用下面这个图概括下:

如何计算元素个数呢?

final long sumCount() {CounterCell[] as = counterCells; CounterCell a;//先取出baseCount的值long sum = baseCount; //再取出counterCells数组中每个单元格的值if (as != null) {for (int i = 0; i < as.length; ++i) {if ((a = as[i]) != null)sum += a.value;}}return sum;
}
复制代码

好了,关于统计元素个数就梳理到这里,接下来我们看下是否扩容的逻辑。

3.5 扩容

private final void addCount(long x, int check) {//......省略统计元素个数的逻辑........//如果是remove元素,则不要走扩容逻辑了,remove传过来的是-1if (check >= 0) {Node<K,V>[] tab, nt; int n, sc;/*** s = sumCount() 就是元素个数* 因为table已经完成了初始化(table != null),所以sizeCtl此时表示扩容阈值* s >= (long)(sc = sizeCtl): 判断集合元素个数是否大于扩容阈值,超过则扩容*/while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&(n = tab.length) < MAXIMUM_CAPACITY) {/*** 内部实现:Integer.numberOfLeadingZeros(n) | (1 << (16 - 1));* 先不看前面,二者按位或之后第17位肯定是1,其他先不管*/int rs = resizeStamp(n);/*** sc < 0 是什么情况?很明显就是扩容了* * 假设a,b两个线程同时来到while这里,a线程优先执行了else if, 此时将会把* sizeCtl修改为负数,此时b线程判断 sc < 0 就成立了。** 假如a线程执行完CAS判断后,还没有执行transfer(tab, null)方法,此时切换* 到b线程,由于nextTable == null, 所以b线程直接退出了。这怎么理解 ?*  1)b线程已经将元素添加到old table中了*  2)元素个数已经统计完毕了* 所以,直接让b线程退出,由a线程完成迁移工作就可以了。。。。。** 假如a线程执行完CAS判断后,开始执行transfer(tab, null)方法,nextTable != null* 此时切换到b线程,b线程执行CAS判断后,执行transfer(tab, nt) 方法,这里怎么理解?* --这里就是协助扩容了。*  **/if (sc < 0) {/*** 判断是否已经扩容结束了,nextTable就是扩容后的新数组* 扩容完成后,会将nextTable 赋给 table*/if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||transferIndex <= 0)break;if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))//协助扩容transfer(tab, nt);}/** 将rs左移16位,使sizeCtl变成负数*/else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2))//当前线程是触发扩容的第一个线程                     transfer(tab, null);s = sumCount();}}
}
复制代码

我们先来看下rs变量的含义:

static final int resizeStamp(int n) {//RESIZE_STAMP_BITS = 16return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
复制代码

首先将1左移15位

00000000 00000000 00000000 00000001
00000000 00000000 10000000 00000000 #左移15位,前面15位去掉,后面补15位
复制代码

不管 Integer.numberOfLeadingZeros(n)的值是什么,他们按位或的结果,其中第17位一定是1.

我们继续看 else if 中的CAS的逻辑:

//RESIZE_STAMP_SHIFT = 16
else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2))
复制代码

其中要对rs的值左移16位:

00000000 00000000 10000000 00000000  #rs
10000000 11100111 00111111 11111000  # rs << 16
复制代码

rs 左移16位,会将第17位的1移动到首位,这意味着什么?意味着rs 是一个小于0的数值。只看首尾就好,其他位是什么不重要。

接下来我们就看下扩容的核心逻辑(非常复杂):

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {int n = tab.length, stride;/*** 这个方法是可以完成多个线程的共同扩容的* 这里的含义就是对旧数组分块,每个线程负责多少块(步长)的数据迁移。* 默认是每个线程负责16个桶位的迁移* 当然了,如果是单核,那就完全没有必要使用多线程扩容了,数组长度是多少,线程就负责多少个元素(步长)*/if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)stride = MIN_TRANSFER_STRIDE; // subdivide rangeif (nextTab == null) {            // initiatingtry {//创建新的数组,长度为原来的2倍Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];nextTab = nt;} catch (Throwable ex) {      // try to cope with OOMEsizeCtl = Integer.MAX_VALUE;return;}//方便协助扩容的线程拿到新表nextTable = nextTab;//transferIndex 就是旧数组的长度transferIndex = n;}int nextn = nextTab.length;/*** 创建一个 fwd 节点,它的hash值固定为 -1*/ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);//迁移推进标识boolean advance = true;boolean finishing = false; // to ensure sweep before committing nextTabfor (int i = 0, bound = 0;;) {Node<K,V> f; int fh;/*** 这个while的逻辑:*  1)给每个线程分配迁移的任务(默认16个长度)*  2) 从后往前一个一个迁移*/while (advance) {int nextIndex, nextBound;/*** boud 是一个任务段的边界值,请看下面的图* 没有达到边界值之前,一个一个元素的迁移*/if (--i >= bound || finishing)advance = false;//迁移到最后一个元素了    else if ((nextIndex = transferIndex) <= 0) {i = -1;advance = false;}/*** 这里的逻辑就是为每个线程分配一个任务段(默认就是16个桶位的长度)**/else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,nextBound = (nextIndex > stride ?nextIndex - stride : 0))) {bound = nextBound;i = nextIndex - 1;advance = false;}}/*** 这个if的判断就是:检查是否所有线程都迁移完成***/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;/*** 再次review旧数组每个节点,确保没有遗漏的* 来到这里,i = oldTable.legnth, bound = 0*/i = n; // recheck before commit}}//从这接下来的判断就是真正的迁移了//如果旧数组位置是null,那干脆不用迁移了,直接放一个fwd节点,告诉其他线程正在扩容,//不要再往旧数组放了,你可以过来协助扩容else if ((f = tabAt(tab, i)) == null)advance = casTabAt(tab, i, null, fwd);//说明已经迁移过了    else if ((fh = f.hash) == MOVED)advance = true; // already processed//迁移真正有数据的桶位    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);elsehn = 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;elseloTail.next = p;loTail = p;++lc;}else {if ((p.prev = hiTail) == null)hi = p;elsehiTail.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;}}}}}
}
复制代码

整个迁移的逻辑非常的复杂,并且代码很长,我们将其拆开看:

3.5.1 对迁移步长的理解

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {int n = tab.length, stride;//构建每个线程迁移的步长,默认每个就是16个if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)stride = MIN_TRANSFER_STRIDE; // subdivide range/*** 穿件扩容后新的table, 长度为原来的2倍*/if (nextTab == null) {            // initiatingtry {@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];nextTab = nt;} catch (Throwable ex) {      // try to cope with OOMEsizeCtl = Integer.MAX_VALUE;return;}//赋给nextTable, 方便其他协助扩容的线程能否拿到新表nextTable = nextTab;transferIndex = n;}int nextn = nextTab.length;/*** 构建一个fwd节点,hash值固定为-1*/ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);boolean advance = true;boolean finishing = false; //从后往前遍历旧数组,每迁移一个就把旧数组的位置放一个fwd节点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;}}//省略for循环的其他逻辑........}  }   
复制代码

我们将重点关注代码截图展示:

  1. 首先线程进来先执行1处,它就是给每个线程分配迁移的任务段,默认就是16个,bound就是本次线程负责迁移的段尾
  2. 然后循环执行2处,从后往前一个一个迁移数据,迁移完成后,将fwd节点覆盖
  3. 表示整体迁移结束了

我们用一个图来表示下它的分段:

我们在用一个图来展示下

3.5.2 迁移数据

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {//....省略其他代码......ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);boolean advance = true;boolean finishing = false; // to ensure sweep before committing nextTabfor (int i = 0, bound = 0;;) {Node<K,V> f; int fh;//1.省略分配任务的while循环//2.省略迁移完成的判断//接下来就是迁移工作了//如果旧数组位置是null,则不用迁移,直接放一个fwd节点即可//如果其他线程想要put元素,发现是fwd节点,则知道在扩容,此时将会参与扩容,将数据放到新数组中else if ((f = tabAt(tab, i)) == null)advance = casTabAt(tab, i, null, fwd);//说明已经迁移过了,当每个线程迁移完成后,它还会重新检查一遍,确保没有遗漏    else if ((fh = f.hash) == MOVED)advance = true; // already processed//这里就是将有数据的节点真正的迁移了    else {/*** 这里要加锁,避免A线程迁移的过程中,B线程往旧的数组里put元素* put先抢到锁,就先添加,迁移线程先抢到锁,则先迁移*/synchronized (f) {if (tabAt(tab, i) == f) {Node<K,V> ln, hn;//处理链表if (fh >= 0) {//fh & n: 如果是0,说明是放到原位置,这个和HashMap一样的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);elsehn = new Node<K,V>(ph, pk, pv, hn);}//放到新数组的【原位置】setTabAt(nextTab, i, ln);//放到新数组的【原位置 + oldCap】位置setTabAt(nextTab, i + n, hn);//旧数组放一个fwd节点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;elseloTail.next = p;loTail = p;++lc;}else {if ((p.prev = hiTail) == null)hi = p;elsehiTail.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;}}}}}
}
复制代码

这里其实相对简单,我们简单画个图来展示下:

3.5.3 迁移完成

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {//....省略其他代码.......for (int i = 0, bound = 0;;) {Node<K,V> f; int fh;while (advance) {int nextIndex, nextBound;if (--i >= bound || finishing)advance = false;/*** 迁移完成nextIndex = -1*/else if ((nextIndex = transferIndex) <= 0) {i = -1;advance = false;}//....省略else if....}/*** 迁移完成的判断* 迁移到最后一个元素,i = -1*/if (i < 0 || i >= n || i + n >= nextn) {int sc;if (finishing) {//所有线程迁移完成了,将nextTable 置为nullnextTable = null;//将迁移后的新数组赋给table,此时table就是新数组了table = nextTab;/*** 计算新的扩容阈值* sizeCtl = 2n - 0.5n = 1.5n = 0.75 * 2n* JDK的开发者将位运算用的真是神乎其神*/sizeCtl = (n << 1) - (n >>> 1);return;}/*** 判断是否所有线程都完成了扩容* sc - 1 是因为,协助扩容的时候sc + 1*/if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)return;finishing = advance = true;/***  将数据长度赋给i,重新检查旧数组的所有位置,确保都迁移完毕,没有遗漏*/i = n; }}//......省略其他代码......}       
复制代码

我们重点看下CAS的判断逻辑:

外层判断(1) 为什么要对sizeCtl减1的操作呢?
这个主要是和协助扩容有关。

我们看下协助扩容的代码:

我的初始容量是64,计算出来的rs值是固定的32793,然后触发扩容的第一个线程A会将sizeCtl修改为负数 -2145845246, 然后参与扩容的线程B会将sizeCtl修改为 -2145845246 + 1,参与扩容的线程C将sizeCtl修改为-2145845246 + 1 + 1, 接下来我们回到扩容完成的逻辑:

所以这个内层判断和外层判断的逻辑就是检查是否所有线程都完成了扩容操作

如果从始至终都只有一个线程参与扩容这个判断也是成立的,因为sizeCtl的值先赋给了sc,然后通过CAS将sc - 1 赋给sizeCtl, 但是sc的值没变,所以内层判断肯定是成立的。

我们在简单看下,put元素时出现hash冲突,并且hash=-1时的协助扩容:

简单看下代码:

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {Node<K,V>[] nextTab; int sc;//double checkif (tab != null && (f instanceof ForwardingNode) &&(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {int rs = resizeStamp(tab.length);//sc = sizeCtl) < 0 :小于0表示扩容while (nextTab == nextTable && table == tab &&(sc = sizeCtl) < 0) {/*** 判断是否扩容完毕了。。   * (sc >>> RESIZE_STAMP_SHIFT) != rs 判断我看在JDK17中拿掉了*/if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || transferIndex <= 0)break;//参与扩容    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {transfer(tab, nextTab);break;}}return nextTab;}return table;
}
复制代码

3.6 put元素过程总结

  1. 计算扰动计算出hash值,这个hash值永远是正数

  2. 如果没有初始化,就调用 initTable() 方法来进行初始化

  3. 如果没有 hash 冲突就直接 CAS 无锁插入

  4. 如果正在扩容,则当前线程协助扩容

  5. 如果存在 hash 冲突,就加锁来保证线程安全,两种情况:一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入

  6. 如果该链表的数量大于阈值8并且数组长度大于64,就要先转换成红黑树的结构

  7. 如果添加成功就调用 addCount() 方法统计 size,并且检查是否需要扩容

4.get()方法分析

理解了put方法,再来看get就非常简单了

public V get(Object key) {Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;//计算key的hash值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;}/*** 从红黑树中查找* hash < 0 只有是扩容或者红黑树在桶中的元素节点*/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;
}
复制代码

5.debug 分析多线程扩容

准备测试数据

/*** @author qiuguan* @date 2022/11/24 23:40:24  星期四*/
public class ConcurrentHashMapTest {public static void main(String[] args) {/*** 指定容量32,则table的初始容量是64* 当达到 64 * 0.75 = 48 时触发扩容*/ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>(32);//先放47个元素for (int i = 1; i <= 47; i++) {chm.put("jk" + i, i);}//Ea 的 hash值是: 2236, 在数组中的索引位置是: 60//FB 的 hash值是: 2236, 在数组中的索引位置是: 60int ea = "Ea".hashCode();int fb = "FB".hashCode();System.out.printf("Ea 的 hash值是: %s, 在数组中的索引位置是: %s\n", spread(ea), (64 -1) & spread(ea));System.out.printf("FB 的 hash值是: %s, 在数组中的索引位置是: %s\n", spread(fb), (64 -1) & spread(fb));//再次添加元素会触发扩容new Thread(() -> chm.put("Ea", 11)).start();new Thread(() -> chm.put("FB", 22)).start();}//将源码中计算hash值的逻辑拿出来sstatic final int spread(int h) {return (h ^ (h >>> 16)) & 0x7fffffff;}
}
复制代码

关键位置打上断点

先让thread0(Ea)线程执行:

然后让thread0开始迁移,迁移几个就行,因为"Ea"的在数组中位置是靠后的(index = 60),而迁移是从后往前迁移的,所以确保"Ea"迁移完成后,我们切换到 "thread1"线程。

thread1线程进行协助扩容前,我先用几张图来演示下过程:

  1. thread0("Ea")触发扩容

  2. thread0("Ea")开始扩容并完成"Ea"数据的迁移工作

  1. thread1("FB")put元素发生hash碰撞并且桶中元素的hash值为-1,准备协助扩容

接下来我们就从"thread1"视角看它是如何协助扩容的,首先要切换到"thread1"线程

用一张图展示的话,大概就是下面这样

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

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

相关文章

异步请求-AJAX

什么是同步交互 首先用户向HTTP服务器提交一个处理请求。接着服务器端接收到请求后&#xff0c;按照预先编写好的程序中的业务逻辑进行处理&#xff0c;比如和数据库服务器进行数据信息交换。最后&#xff0c;服务器对请求进行响应&#xff0c;将结果返回给客户端&#xff0c;返…

黑苹果之微星(MSI)主板BIOS详细设置篇

很多童鞋安装黑苹果的时候会卡住&#xff0c;大部分原因是cfg lock 没有关闭&#xff0c;以及USB端口或SATA模式设置错误。 为了避免这些安装阶段报错的情况发生&#xff0c;今天给大家分享一下超详细的BIOS防踩坑设置指南--微星&#xff08;MSI&#xff09;主板BIOS篇&#xf…

CTFHub | Refer注入

0x00 前言 CTFHub 专注网络安全、信息安全、白帽子技术的在线学习&#xff0c;实训平台。提供优质的赛事及学习服务&#xff0c;拥有完善的题目环境及配套 writeup &#xff0c;降低 CTF 学习入门门槛&#xff0c;快速帮助选手成长&#xff0c;跟随主流比赛潮流。 0x01 题目描述…

天然气潮流计算matlab程序

天然气潮流计算matlab程序 1 天然气潮流计算理论 由于天然气涉及到流体的运动方程&#xff0c;直接计算非常复杂&#xff0c;因此需要提前做出一些假设来简化计算&#xff0c;经过研究&#xff0c;适当的假设对结果影响很小&#xff0c;因此本文对天然气系统做出如下假设&#…

MNN--初步学习

来自阿里MNN有三个贡献点&#xff1a; 提出了预推理机制&#xff0c;在线计算推理成本和最优方案优化了kernel提出后端抽象实现混合调度MNN的架构&#xff1a; 分离线和在线两部分。离线就是很传统的模型转换、优化、压缩、量化的那一套东西&#xff0c;这里mnn转出的模型文件…

springboot thymeleaf使用

导入依赖 <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf</artifactId> <version>3.0.11.RELEASE</version> </dependency> <dependency> <groupId>org.thymeleaf</groupId> <a…

人口数据集:地级市常住人口与户籍人口、人口1%抽样调查数据两大维度指标数据

一、地级市常住人口与户籍人口 1、数据来源&#xff1a;地级市常住人口数据&#xff08;主要来源于各地政府公报&#xff09;&#xff0c;户籍人口数据来源于《中国城市统计年鉴》 2、时间跨度&#xff1a;2003-2019年 3、区域范围&#xff1a;280个地级市 4、指标说明&…

牛顿法(牛顿拉夫逊)配电网潮流计算matlab程序

牛顿法配电网潮流计算matlab程序 传统牛顿—拉夫逊算法&#xff0c;简称牛顿法&#xff0c;是将潮流计算方程组F(X)0&#xff0c;进行泰勒展开。因泰勒展开有许多高阶项&#xff0c;而高阶项级数部分对计算结果影响很小&#xff0c;当忽略一阶以上部分时&#xff0c;可以简化对…

校园论坛设计(Java)——介绍篇

校园论坛设计&#xff08;Java&#xff09; 文章目录校园论坛设计&#xff08;Java&#xff09;0、写在前面1、项目介绍2、项目背景3、项目功能介绍3.1 总体设计图3.2 帖子模块3.3 学习模块3.4 个人信息模块3.5 数据报表模块3.6 校园周边模块3.7 用户管理模块3.8 登录注册模块4…

[足式机器人]Part3机构运动微分几何学分析与综合Ch02-3 平面机构离散运动鞍点综合——【读书笔记】

本文仅供学习使用 本文参考&#xff1a; 《机构运动微分几何学分析与综合》-王德伦、汪伟 《微分几何》吴大任 Ch02-3 平面机构离散运动鞍点综合2.4 鞍滑点2.4.1 鞍线与二副连架杆P-R2.4.2 鞍线误差2.4.3 三位置鞍线2.4.4 四位置鞍线2.4.5 多位置鞍线2.4.6 滑点与鞍滑点2.4 鞍滑…

问题盘点|使用 Prometheus 监控 Kafka,我们该关注哪些指标

Kafka 作为当前广泛使用的中间件产品&#xff0c;承担了重要/核心业务数据流转&#xff0c;其稳定运行关乎整个业务系统可用性。本文旨在分享阿里云 Prometheus 在阿里云 Kafka 和自建 Kafka 的监控实践。01Kafka 简介Aliware01Kafka 是什么&#xff1f;Kafka 是分布式、高吞吐…

力扣(LeetCode)88. 合并两个有序数组(C++)

朴素思想 朴素思想&#xff0c;开第三个数组&#xff0c;对 nums1nums1nums1 和 nums2nums2nums2 进行二路归并。 class Solution { public:void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {vector<int> nums3(mn);int i 0,j …

创新赋能合作伙伴,亚马逊云科技re:Invent科技盛宴

北京时间11月29号&#xff0c;亚马逊云科技年度峰会re:Invent 2022将在拉斯维加斯开幕。这场年度最重磅的云计算技术大会不仅是科技盛宴&#xff0c;也是亚马逊云科技与诸多客户交流互鉴的绝佳平台&#xff0c;今天带大家认识一下几位资深云计算用户&#xff0c;以及他们和re:I…

Pytorch 中Label Smoothing CrossEntropyLoss实现

一. 前言 一般情况下我们都是直接调用Pytorch自带的交叉熵损失函数计算loss&#xff0c;但涉及到魔改以及优化时&#xff0c;我们需要自己动手实现loss function&#xff0c;在这个过程中如果能对交叉熵损失的代码实现有一定的了解会帮助我们写出更优美的代码。 其次是标签平…

【架构设计】作为架构师你应该掌握的画图技术

1.前言 大家知道&#xff0c;架构的过程其实就是建模的过程&#xff0c;那自然离不开架构图。那么&#xff0c;我们先来看几个问题。 &#xff08;1&#xff09;什么是架构图&#xff1f; 架构图 架构 图&#xff0c;用图的形式把系统架构展示出来&#xff0c;配上简单的文…

基于C#的校园闲置物品共享系统的开发和实现(Asp.net+Web)

目 录 摘 要 I Abstract II 第1章 绪论 1 1.1选题背景 1 1.1.1校园闲置物品共享系统的开发背景 1 1.1.2学生闲置物品交易活动的现状 1 1.2 校园闲置物品共享系统的研究方向和内容 1 1.2.1研究方向 1 1.2.2研究内容 2 1.3 校园闲置物品共享系统的设计目标 2 1.4 校园闲置物品共…

多云加速云原生数仓生态,华为与 HashData 联合打造方案

多云的兴起&#xff0c;源于用户应用对于基础设施、云服务功能、安全性等的差异化需求&#xff0c;用户希望根据需求将应用、数据因“云”制宜&#xff0c;实现业务的高度灵活性和高效性。这也直接驱动着云原生数据仓库等一批云原生应用的流行&#xff0c;以及存储等基础设施加…

WR | 水源水耐药基因稳定赋存的关键:以致病菌为“源”,群落构建主导菌为“汇”...

第一作者&#xff1a;武冬通讯作者&#xff1a;David W.Graham、杨凯、谢冰通讯单位&#xff1a;华东师范大学生态与环境科学学院&#xff0c;英国纽卡斯尔大学工程学院文章链接&#xff1a;www.sciencedirect.com/science/article/pii/S0043135422013045- 成果简介 -近日&…

【食品加工技术】第五章 烘烤食品加工技术 笔记

【食品加工技术】第五章 烘烤食品加工技术 笔记5.1 焙烤食品概述烘烤食品的分类按发酵和膨化程度分类安装生产工艺分类烘烤食品的原料面粉糖蛋品乳及乳制品膨松剂烘烤设备常用设备恒温设备常用工具5.2 面包加工工艺和关键技术面包的分类面包的发酵原理面包的工艺流程一次发酵二…

HTML CSS个人网页设计与实现——人物介绍丁真(学生个人网站作业设计)

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…