JDK8 ConcurrentHashMap源码分析
文章目录
- 常量说明
- put() 方法
- putVal() 方法
- initTable():初始化数组
- treeifyBin():链表转红黑树
- tryPresize():初始化数组+扩容
- TreeBin() 构造方法:生成红黑树
- putTreeVal():往红黑树中插入值
- helpTransfer():多线程帮助扩容
- addCount():计算Map中的元素总数(put时+1,delete时-1)
- fullAddCount():CAS往CounterCell数组中加值
- sumCount():计算Map中所有元素总数
- resizeStamp():当返回值左移 RESIZE_STAMP_SHIFT 位时,一定是负数
- transfer():将旧数组元素转移到新数组中
- ForwardingNode():构造方法,hash值为-1(MOVED)
- 总结
- JDK8中的ConcurrentHashMap是怎么保证并发安全的?
常量说明
数组中 Node 节点的 Hash 值几种常量:
// hash>0表示数组中是链表,hash=0表示null
static final int MOVED = -1; // Map 在扩容,旧数组中对应节点已经被到了新数组中
static final int TREEBIN = -2; // 数组对应节点是红黑树(与HashMap不同,hashMap数组中存放的是红黑树的根节点,// 而ConcurrentHashMap给红黑树包了一层,放的是TreeBin对象,hash值为-2,其中有一个指针指向红黑树的根节点)
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
put() 方法
public V put(K key, V value) {return putVal(key, value, false);
}
putVal() 方法
final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException(); // k,v不能为空int hash = spread(key.hashCode()); // 计算hash值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) { //数组对应位置的值为null,tabAt方法保证调用时的可见性if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))) // cas 往里设置值break; // no lock when adding to empty bin}else if ((fh = f.hash) == MOVED) // Map正在扩容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;}}//HashMap:数组中是放入了红黑树的root节点,TreeNode类型//ConcurrentHashMap:数组中是放入了TreeBin对象,TreeBin对象中有一个root属性,指向红黑树root节点//相当于是给红黑树在外面包了一层,方便用数组中的节点加锁,避免了HashMap红黑树root节点会变动问题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) //链表数量大于等于8时,转红黑树treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}addCount(1L, binCount);return null;
}
initTable():初始化数组
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(); //让出cpu资源else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //cas获取锁try {if ((tab = table) == null || tab.length == 0) {int n = (sc > 0) ? sc : DEFAULT_CAPACITY; //默认容量16@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; //初始化数组table = tab = nt;sc = n - (n >>> 2); //n右移2位相当于16/4(容量的1/4),然后再用容量减掉,就剩下3/4了,和HashMap的0.75一样}} finally {sizeCtl = sc;}break;}}return tab;
}
treeifyBin():链表转红黑树
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) //数组大小小于64,扩容tryPresize(n << 1);else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {synchronized (b) {if (tabAt(tab, index) == b) {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); //Node节点转换成TreeNode节点// 构建双向链表if ((p.prev = tl) == null)hd = p;elsetl.next = p;tl = p;}setTabAt(tab, index, new TreeBin<K,V>(hd)); //构建红黑树并放入数组中}}}}
}
tryPresize():初始化数组+扩容
private final void tryPresize(int size) {int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :tableSizeFor(size + (size >>> 1) + 1);int sc;while ((sc = sizeCtl) >= 0) {Node<K,V>[] tab = table; int n;if (tab == null || (n = tab.length) == 0) { // 数组初始化,与initTable()方法相似n = (sc > c) ? sc : c;if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {try {if (table == tab) { @SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = nt;sc = n - (n >>> 2);}} finally {sizeCtl = sc;}}}else if (c <= sc || n >= MAXIMUM_CAPACITY) //扩容完毕break;else if (tab == table) { //多线程扩容,与addCount()方法中逻辑相同int rs = resizeStamp(n);if (sc < 0) {Node<K,V>[] nt;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);}}
}
TreeBin() 构造方法:生成红黑树
与 HashMap
中 treeify()
的处理一样,就不再赘述,详见:HashMap put() 方法源码分析#treeify()
// b 是双向链表的头节点
TreeBin(TreeNode<K,V> b) {super(TREEBIN, null, null, null); //设置Hash值为-2,表示一颗红黑树this.first = b;TreeNode<K,V> r = null;for (TreeNode<K,V> x = b, next; x != null; x = 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) ||(dir = compareComparables(kc, k, pk)) == 0)dir = tieBreakOrder(k, pk);TreeNode<K,V> xp = p;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); // 与HashMap处理相同break;}}}}this.root = r;assert checkInvariants(root);
}
putTreeVal():往红黑树中插入值
final TreeNode<K,V> putTreeVal(int h, K k, V v) {// 前面的代码和HashMap的代码一样,就不赘述了Class<?> kc = null;boolean searched = false;for (TreeNode<K,V> p = root;;) {int dir, ph; K pk;if (p == null) {first = root = new TreeNode<K,V>(h, k, v, null, null);break;}else if ((ph = p.hash) > h)dir = -1;else if (ph < h)dir = 1;else if ((pk = p.key) == k || (pk != null && k.equals(pk)))return p;else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0) {if (!searched) {TreeNode<K,V> q, ch;searched = true;if (((ch = p.left) != null &&(q = ch.findTreeNode(h, k, kc)) != null) ||((ch = p.right) != null &&(q = ch.findTreeNode(h, k, kc)) != null))return q;}dir = tieBreakOrder(k, pk);}TreeNode<K,V> xp = p;if ((p = (dir <= 0) ? p.left : p.right) == null) {TreeNode<K,V> x, f = first;first = x = new TreeNode<K,V>(h, k, v, f, xp);if (f != null)f.prev = x;if (dir <= 0)xp.left = x;elsexp.right = x;if (!xp.red) //父节点是黑的,无需加锁,直接插入x.red = true;else {lockRoot(); //加写锁try {root = balanceInsertion(root, x);} finally {unlockRoot();}}break;}}assert checkInvariants(root);return null;
}
helpTransfer():多线程帮助扩容
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {Node<K,V>[] nextTab; int sc;if (tab != null && (f instanceof ForwardingNode) &&(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {int rs = resizeStamp(tab.length);while (nextTab == nextTable && table == tab &&(sc = sizeCtl) < 0) {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;
}
addCount():计算Map中的元素总数(put时+1,delete时-1)
private final void addCount(long x, int check) { //x:1;check:链表长度或者红黑树是2CounterCell[] as; long b, s;if ((as = counterCells) != null ||!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { //尝试直接往baseCount中加值// CounterCell数组不为空(说明有竞争) 或者 直接往baseCount中加值失败(cas失败)CounterCell a; long v; int m;boolean uncontended = true;if (as == null || (m = as.length - 1) < 0 || // 数组为空(a = as[ThreadLocalRandom.getProbe() & m]) == null || //获取线程的随机数 & 数组长度求出对应线程的数组下标,as数组中对应下标的元素为null!(uncontended =U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { //cas往as数组中对应位置加1fullAddCount(x, uncontended); //CAS往里加数组里加1return; //进入这个if虽然加了1,但是不会走后面的扩容逻辑了(有冲突时不执行扩容,没冲突且达到阈值时才扩容)}if (check <= 1)return;s = sumCount(); //算出当前map中元素个数}if (check >= 0) {Node<K,V>[] tab, nt; int n, sc;while (s >= (long)(sc = sizeCtl) && (tab = table) != null && //大于阈值(sizeCtl)时扩容(n = tab.length) < MAXIMUM_CAPACITY) {int rs = resizeStamp(n);if (sc < 0) { // sc是负数说明正在扩容,与helpTransfer()方法逻辑相同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)) //每有一个后续线程帮助扩容时,sc就+1transfer(tab, nt);}else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2)) //注意,第一个线程执行扩容时,会把sc设置成负数并+2(初始值)transfer(tab, null);s = sumCount();}}
}
ThreadLocalRandom.getProbe()
方法详解:关于 ConcurrentHashMap 1.8 中的线程探针哈希
fullAddCount():CAS往CounterCell数组中加值
//wasUncontended字段用来标识当前线程获取的probe通过计算后,在CounterCell数组中对应下标是否能加成功
private final void fullAddCount(long x, boolean wasUncontended) { //x:1;wasUncontended:falseint h;if ((h = ThreadLocalRandom.getProbe()) == 0) { //获取线程的hash值(多次调用值一样)ThreadLocalRandom.localInit(); //初始化h = ThreadLocalRandom.getProbe();wasUncontended = true;}boolean collide = false; // true时表示发生冲突,数组要执行扩容for (;;) { //自旋CounterCell[] as; CounterCell a; int n; long v;if ((as = counterCells) != null && (n = as.length) > 0) { // 数组不是空的,已经完成了初始化if ((a = as[(n - 1) & h]) == null) { //数组当前下标位置是空的if (cellsBusy == 0) { // Try to attach new CellCounterCell r = new CounterCell(x); // 初始化数组中对应位置的元素,且初始值为1if (cellsBusy == 0 &&U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { // 数组cas加锁boolean created = false;try { // Recheck under lockCounterCell[] 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; // 继续循环(用continue跳过了修改线程的probe值)} }collide = false;}else if (!wasUncontended) // false表示上一次cas+1的时候失败了wasUncontended = true; // 刷新状态为true,并修改线程probe的值(跳出if后)else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)) //再尝试cas+1,成功则跳出循环,失败则修改线程probe的值break;else if (counterCells != as || n >= NCPU) //数组扩容后或者数组长度大于cpu核数时,不允许扩容collide = false; // At max size or staleelse if (!collide) //CAS失败,说明发生冲突,collide设置为true,下次循环过来collide 为true时,执行扩容collide = true;else if (cellsBusy == 0 &&U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { //数组加锁,扩容try {if (counterCells == as) {// Expand table unless staleCounterCell[] rs = new CounterCell[n << 1]; //创建新数组,容量*2for (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); //给线程生成一个新的Probe值(随机数)}else if (cellsBusy == 0 && counterCells == as && //cellsBusy标识数组是否被占用,0表示没有占用U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { //cas将cellsBusy改为1(加锁),保证只有一个线程能对cells数组初始化boolean init = false;try { // Initialize tableif (counterCells == as) {CounterCell[] rs = new CounterCell[2]; //初始化数组大小为2rs[h & 1] = new CounterCell(x); //对应位置直接+1counterCells = rs;init = true; //初始化完毕}} finally {cellsBusy = 0;}if (init) //初始化完毕,且计数也加了1,就跳出循环break;}else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x)) //如果其它情况都被其它线程占用,则尝试对baseCount加1break; // Fall back on using base}
}
sumCount():计算Map中所有元素总数
final long sumCount() {CounterCell[] as = counterCells; CounterCell a;long sum = baseCount; //基础值if (as != null) {for (int i = 0; i < as.length; ++i) { //遍历CounterCell数组进行统计求和if ((a = as[i]) != null)sum += a.value;}}return sum;
}
resizeStamp():当返回值左移 RESIZE_STAMP_SHIFT 位时,一定是负数
private static int RESIZE_STAMP_BITS = 16;private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;//resizeStamp相当于把第int的第16位赋值为1(1左移15位是在第16位),然后在addCount方法中向左移动16位时,1到了符号位,所以变成负数了//RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS,所以只要 1 左移RESIZE_STAMP_BITS-1位,再左移RESIZE_STAMP_SHIFT位
//(共左移了31位,1到了第32位),1就被移到了符号位,成为了负数
static final int resizeStamp(int n) {return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
Integer.numberOfLeadingZeros(i)
方法:返回无符号整数 i
的最高非 0
位前面的 0
的个数,包括符号位在内;如果 i
为负数,这个方法将会返回 0
transfer():将旧数组元素转移到新数组中
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { //tab是旧数组,nextTab是新数组int n = tab.length, stride;if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) //计算步长,(n/8)/NCPU,最小值是16stride = MIN_TRANSFER_STRIDE; // subdivide rangeif (nextTab == null) { // 新数组为空,初始化try {@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; //扩容,*2nextTab = nt;} catch (Throwable ex) { // try to cope with OOMEsizeCtl = Integer.MAX_VALUE;return;}nextTable = nextTab;transferIndex = n; //transferIndex:多线程下,数组从后往前遍历挨个元素转换,transferIndex 表示下个线程应该转换旧数组的哪个元素}int nextn = nextTab.length;ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); //ForwardingNode是个特殊的Node对象,hash值为MOVED(-1)boolean advance = true; //当前线程是否继续往前找未转移的元素boolean finishing = false; // 当前线程的扩容逻辑是否做完(只是当前线程)for (int i = 0, bound = 0;;) { Node<K,V> f; int fh;while (advance) {int nextIndex, nextBound; //nextIndex是跳过的右边界,nextBound是跳过的左边界,左闭右开//不越界的情况下,nextIndex - nextBound = stride(步长)if (--i >= bound || finishing) //倒着遍历advance = false;else if ((nextIndex = transferIndex) <= 0) { //旧数组已经转换到下标0的位置了i = -1;advance = false;}else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,nextBound = (nextIndex > stride ?nextIndex - stride : 0))) { //数组是从右往左遍历,如果越界(负数),下标就设置为0bound = 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); // *2*0.75return;}if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { //每有一个线程帮助扩容完,sc就-1if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)//这个逻辑相当于: sc != (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 , //(resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 是第一个线程执行扩容时,sc 设置的初始值,之后每有一个线程帮忙扩容,就在 sc 的初始值上 + 1return; //当前线程扩容执行完毕// 当 sc == (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 时,会走到这儿,表示除了当前线程,其它所有帮助扩容的线程都执行完了finishing = advance = true; //表示所有线程扩容执行完毕i = n; // recheck before commit}}else if ((f = tabAt(tab, i)) == null) //数组对应位置是nulladvance = 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) { //数组中对应位置是链表// 链表在扩容时会分成高位和低位(和HashMap相同),假设单向链表最后一位是高位,然后往前推,反向遍历,runBit就指向链表节点第一次变成低位时的后一个高位节点// 即在原链表中,runBit指向的节点为头的单向链表,要么都是低位的,要么都是高位的int runBit = fh & n; //指向链表最后相同位的节点Node<K,V> lastRun = f;for (Node<K,V> p = f.next; p != null; p = p.next) { //遍历链表,找出runBit节点的位置int b = p.hash & n;if (b != runBit) {runBit = b;lastRun = p;}}if (runBit == 0) { //将runBit后的节点整个挪到新数组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); //往旧数组中设置fwdadvance = true;}else if (f instanceof TreeBin) { //如果数组中的元素是红黑树,与HashMap逻辑相同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) : //小于6,红黑树退化成链表(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;}}}}}
}
ForwardingNode():构造方法,hash值为-1(MOVED)
ForwardingNode(Node<K,V>[] tab) {super(MOVED, null, null, null);this.nextTable = tab;
}
总结
JDK8中的ConcurrentHashMap是怎么保证并发安全的?
主要利用 Unsafe
操作 + synchronized
关键字。
Unsafe
操作的使用主要负责并发安全的修改对象的属性或数组某个位置的值。synchronized
主要负责在需要操作某个位置时进行加锁(该位置不为空),比如向某个位置的链表进行插入结点,向某个位置的红黑树插入结点。
JDK8中仍然有分段锁的思想,JDK8中数组的每一个位置都有一把锁。
当向 ConcurrentHashMap
中 put
一个 key
,value
时,
-
首先根据
key
计算对应的数组下标i
,如果该位置没有元素,则通过自旋的方法去向该位置赋值。 -
如果该位置有元素,则
synchronized
会加锁 -
加锁成功之后,在判断该元素的类型
a. 如果是链表节点则进行添加节点到链表中
b. 如果是红黑树则添加节点到红黑树 -
添加成功后,判断是否需要进行树化
-
addCount()
,这个方法的意思是ConcurrentHashMap
的元素个数加1
,但是这个操作也是需要并发安全的,并且元素个数加1
成功后,会继续判断是否要进行扩容,如果需要,则会进行扩容,所以这个方法很重要。 -
同时一个线程在
put
时如果发现当前ConcurrentHashMap
正在进行扩容则会去帮助扩容。
相关文章:
JDK8 ConcurrentHashMap源码分析
文章目录常量说明put() 方法putVal() 方法initTable():初始化数组treeifyBin():链表转红黑树tryPresize():初始化数组扩容TreeBin() 构造方法:生成红黑树putTreeVal():往红黑树中插入值helpTransfer():多线…...
前置知识-初值问题、欧拉法、改进欧拉法
1.1 初值问题 初值问题是科研、工程技术应用中最常见的一类问题, 一阶常微分方程的初值问题表述如下: 已知 u ( x ) u(x) u(x) 的起始点 ( x 0 , u 0 ) \left(x_0, u_0\right)...
睡眠影响寿命,这几个睡眠习惯赶紧改掉!
我们知道,现在睡眠不足已经成为普遍问题,但你知道睡眠的时长会影响寿命吗?熬夜对身体不好,已是老生常谈。但睡得过早,也可能影响寿命!2021年《睡眠医学》杂志一项针对21个国家11万名参与者的研究中发现&…...
Linux逻辑卷管理器(PV、VG、LV、PE)
目录 PV阶段 VG阶段 LV阶段 文件系统阶段 逆向操作(删除LVM) 逻辑卷管理器(Logical Volume Manager),简称LVM LVM的做法是将几个物理的分区(或磁盘)通过软件组合成为一块看起来时独立的大…...
Centos7 内核升级
一、背景 在 CentOS 使用过程中,高版本的应用环境可能需要更高版本的内核才能支持,所以难免需要升级内核,所以下面将介绍yum和rpm两种升级内核方式。 关于内核种类: kernel-ml——kernel-ml 中的ml是英文【 mainline stable 】的缩写&…...
SpringBoot 启动配置文件加载和参数配置修改问题
SpringBoot 配置文件修正和参数覆盖SpringBoot 配置文件加载和参数覆盖1、SpringBoot 配置文件加载1.1、修改application.properties的参数几种方式1.2、方法一:直接CMD1.3、方法二:系统变量配置1.4、方法三:程序运行配置1.5、方法四…...
布隆过滤器和布谷鸟过滤器详解
今天和大家分享下布隆过滤器和布谷鸟过滤器 一.布隆过滤器 1.简单介绍 布隆过滤器是用于检索一个元素是否在一个集合中的算法,是一种用空间换时间的查询算法。 2.实现原理 布隆过滤器的存储结构是一个bitmap结构,初始值都是0,如下图所示&am…...
WebGIS前端框架(openlayers,mapbox,leaflet)图形图像底层渲染原理分析
学了这么多的框架,做了这么多的项目,你是否清楚你使用的GIS框架(mapbox,open layers,cesium,leaflet)底层到底是什么原理?是否清楚哪些所谓的地图影像,矢量图形,图标,图像动画等是如何渲染到网页上的?这篇文章就大家解读一下WebGIS的底层原理。 首先说说历史,有时…...
AcWing语法基础课笔记 第五章 C++中的字符串
第五章 C中的字符串 字符串是计算机与人类沟通的重要手段。 ——闫学灿 字符与整数的联系——ASCII码 每个常用字符都对应一个-128~127的数字,二者之间可以相互转化: 常用ASCII值:’A’-‘Z’ 是65~90,’a’-‘z’…...
抓包工具Charles(一)-下载安装与设置
无论是在测试、开发工作中,抓包都是很重要、很常用的技能。Charles作为一款抓包工具,能够满足大部分的工作需求。 文章目录一、下载地址二、安装三、安装根证书(电脑)四、设置五、抓包附录:[零基础入门接口功能测试教程…...
SpringBoot09:Swagger
什么是Swagger? ①是一个API框架 ②可以在线自动生成 RestFul 风格的API文档,实现API文档和API定义同步更新 ③可以直接运行、在线测试 API 接口 ④支持多种语言(Java、PHP等) 官网:API Documentation & Desi…...
Git 常用命令
笔记-git命令1、名词2、基本操作3、分支操作1、名词 master: 默认开发分支origin: 默认远程版本库Index / Stage: 暂存区Workspace: 工作区Repository: 仓库区 (或本地仓库)Remote: 远程仓库 2、基本操作 配置级别 -local (默认,高级优先…...
查看jdk安装路径,在windows上实现多个java jdk的共存解决办法,安装java19后终端乱码的解决
查看jdk安装路径, 在windows上实现多个java jdk的共存解决办法, 安装java19后终端乱码的解决 目录 一、查看jdk(java开发工具包)安装路径的方法 二、在windows上实现多个java jdk的共存 (1)、安装好多…...
链表数据结构
用途: 链表是一种用于计算机中存储与组织数据的结构,链表将数据以节点的形式串联起来,其存储的容量大小可以动态伸缩。 结构: typedef struct {int data; /*当前节点的数据*/node *next;/*下一个节点的指针*/node *last;/*上一个…...
汽车DTC故障内码与标准故障码的解析与转换
目录 一、故障内码与标准故障码的解析 (1)故障内码的信息格式与解析 (2)故障内码中DTC状态的解析 (3)故障内码与标准故障码之间的对应关系 二、故障内码与标准故障码的转换代码 一、故障内码与标准故障…...
零基础学习测试还是开发?
软件测试作为IT行业的刚需职位,其实是非常适合0基础的小白同学加入学习的但是具体选择测试还是开发还是要看你个人的兴趣爱好以及学习能力,对哪个感兴趣,哪个能学的会就选择哪个就可以了 平时说起程序员印象中大都是做Java、做前端、做后端&…...
如何加入new bing候补名单
如何加入new bing候补名单 我们都知道现在最新版edges中已经提示我们可以加入new bing候补名单,但国内环境下无法正常加入new bing候补名单,这篇文章讲告诉你如何绕过限制加入new bing候补名单 下载配置 HeaderEditor 插件 下载地址microsoftedge.mic…...
中国天气——西风带环流和寒潮
中国天气——西风带环流和寒潮 一. 西风环流概述 1. 概念 西风带:中高纬度地区平均水平环流在对流层盛行西风,称之为西风带西风带波动:西风带围绕极涡沿纬圈运动,平均而言表现为冬季三槽三脊,夏季四槽四脊ÿ…...
2022黑马Redis跟学笔记.实战篇(四)
2022黑马Redis跟学笔记.实战篇 四4.3.秒杀优惠券功能4.3.1.秒杀优惠券的基本实现一、优惠卷秒杀1.1 全局唯一ID1.2 Redis实现全局唯一Id1.3 添加优惠卷1.4 实现秒杀下单4.3.2.超卖问题4.3.3.基于乐观锁解决超卖问题1. 悲观锁2. 乐观锁3. 乐观锁解决超卖问题4.4 秒杀的一人一单限…...
Allegro中如何删除多余D码操作指导
Allegro中如何删除多余D码操作指导 用Allegro做PCB设计的时候,在最后输出生产文件的时候,必须清除多余的D码,不让多余的D码出现在D码文件中,类似下图 如何清除多余D码,具体操作如下 点击Tools点击Padstack...
学生投票系统-课后程序(JAVA基础案例教程-黑马程序员编著-第三章-课后作业)
【案例3-4】学生投票系统 记得 关注,收藏,评论哦,作者将持续更新。。。。 【案例介绍】 案例描述 某班级投票竞选班干部,班级学生人数为100人,每个学生只能投一票。 本任务要求,编程实现一个投票程序&…...
初始化一个列表python
1.初始化递增的list: list1 list(range(10)) #print list1 #[0,1,2,...,9] 2.初始化每项为0的一维数组: list2 [0] * 5 #print list2 #[0,0,0,0,0] 3.初始化固定值的一维数组: initVal 1 listLen 5 list3 [ initVal for i in range(5)] …...
【electron】webview嵌入页面发送消息给父级页面
场景需求: 嵌入页面操作时,通知父级页面 涉及知识点: contextBridge 嵌入页面可使用暴露的对象ipc-message 监听嵌入页面发送的消息webview preload 嵌入页面运行加载的脚本 问题(两种方式) 使用监听ipc-message需…...
Whids:一款针对Windows操作系统的开源EDR
关于Whids Whids是一款针对Windows操作系统的开源EDR,该工具所实现的检测引擎基于先前的 Gene项目构建,并专门设计可以根据用户定义的规则匹配Windows事件。 功能特性 1、为社区提供一款功能强大且开源的Windows EDR; 2、支持检测规则透明化…...
初级调色转档CameraRaw
一级调色 还原-曝光-色彩-细节-质感 修图的范围 整体(掌握基本面板)——局部(曲线)——具象(混色器) 修片最开始的准备工作 看直方图:明暗跟色彩的数据表 分析图片是否存在以下问题: 1.曝光…...
Mybatis源码(3) - Executor执行过程 | 一级缓存 | 二级缓存
0. 前言:1. CachingExecutor#query:1.1. BoundSql:1.2. CacheKey:1.3. 二级缓存:1.4. 一级缓存:2. JDBC过程执行:3. 结果集处理:4. Mybatis的一级缓存、二级缓存区别:0. …...
成为 Seatunnel 源码贡献者保姆级教程
Apache SeaTunnel 是下一代高性能、分布式、海量数据集成平台,已经在 B 站、腾讯云等 100 家公司生产使用。目前处于 incubator 阶段。作为公司内部使用的 ETL 工具,Seatunnel 可以基于已有的 Spark、Flink 计算平台进行数据交换也可以运行在 k8s 平台上…...
MySQL的索引视图练习题
学生表:Student (Sno, Sname, Ssex , Sage, Sdept) 学号,姓名,性别,年龄,所在系 Sno为主键 课程表:Course (Cno, Cname,) 课程号,课程名 Cno为主键 学生选课表:SC (Sno, Cno, Score)…...
【C++ Primer Plus】第四章:复合类型
文章目录4.1 数组C11数组初始化的方法4.2 字符串**cin是如何确定已完成字符串输入呢?****如何每次读取一行字符串输入?****面向行的输入:getline()****面向行的输入:get( )****为什么推荐使用get( ),而不是getline( )呢…...
做外贸,你不能不懂的外贸流程知识
报关是履行海关进出境手续的必要环节之一,涉及两大类:进出境运输工具、物品和货物。由于性质不同,报关手续也有些不同。今天我就为大家详细介绍一下进出口报关的流程,包括出口货物报关的流程,随报关单提交的运费和商业单据&#x…...
做网站正规公司/全网媒体发布平台
nginx指定多个域名跨域请求配置什么是跨域假设我们页面或者应用已在 http://www.test1.com 上了,而我们打算从 http://www.test2.com 请求提取数据。一般情况下,如果我们直接使用 AJAX 来请求将会失败,浏览器也会返回“源不匹配”的错误&…...
甘肃建设体网站首页/网络营销
目前项目中使用的是elasticsearch-1.5.1版本,使用到的插件如下: 1. hq 监控,管理elasticsearch集群以及通过web界面来进行查询操作 项目地址: https://github.com/royrusso/elasticsearch-HQ2. analysis-ik ik分词器,中…...
网站开发给网站设置图标在什么文件中写代码/nba最新交易新闻
需要实现的效果如图,当光标停留在System上时出现文档说明,以下jdk1.8举例 实现: 1、先下载一个jdk api 1.8_google.CHM文件 2、cmd中执行命令 先进入该目录下,然后执行下面命令,其中html1.8文档可以自定义࿰…...
网站dns解析失败/代推广app下载
文章目录 本课题的研究内容:探地雷达原理探地雷达图像预处理图像倾斜矫正均值法去背景原理与实现图像分割技术阈值分割技术的实现腐蚀与膨胀技术探地雷达杂波抑制研究与实现探地雷达合成孔径成像探地雷达目标识别总结本文为论文解读,为2008年发布的基于传统图像处理与识别论文…...
商标设计网站图/网络推广公司企业
1.应用场景 主要用于学习计算机网络中的网络I/O的各种模型,以及各自的优缺点,使用场景。 2.学习/操作 1.文档阅读 2021-11-02 - 为什么网络 I/O 会被阻塞?_穿素白衫的少年的博客-CSDN博客 计算机组成原理/计算机网络 - 网卡 - 探究其工作原理…...
贵州贵阳网站开发/如何推广网站运营
今天给大家介绍一下经典的开源机器学习软件: 编程语言:搞实验个人认为当然matlab最灵活了(但是正版很贵),但是更为前途的是Python(numpyscipymatplotlib)和C/C,这样组合既可搞研究,也…...