ConcurrentHashMap

看前

本博客只是为了记录,学习流程,学习源码其大部分看的是ConcurrentHashMap 1.8 源码分析这位大佬的博客,写的十分详细,图文并茂非常易懂,十分推荐。我这篇博客只是在他的基础上加了点自己的理解(肯定有不少理解不到位的地方)以及查询了一些没讲到的函数。

介绍

源码: java.util.concurrent.ConcurrentHashMap
HashMap有线程安全问题,导致出现链表死循环的情况。HashTable和ConcurrentHashMap没有线程安全问题,但二者上锁的方式不同,一个是HashTable全局一把锁,使用的是synchronized,ConcurrentHashMap是分段锁只锁住一个SegMent对象,其Segment对象继承于ReentrantLock。
JDK7: segment+Hashentry+reentrantlock
JDK8: synchronized+CAS
底层数据结构JDK7是segment数组+hashentry链表,JDK8是Node,treebin(红黑树),导致了二者的遍历时间复杂度有时不同。

源码分析

有了前面的HashMap基础,我们在讲解ConcurrentHashMap时只讲解二者不同之处。

主要属性

private static final int MAXIMUM_CAPACITY = 1 << 30;
private static final int DEFAULT_CAPACITY = 16;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
static final int MOVED     = -1; // 表示正在转移
static final int TREEBIN   = -2; // 表示已经转换成树
static final int RESERVED  = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
transient volatile Node<K,V>[] table;//默认没初始化的数组,用来保存元素
private transient volatile Node<K,V>[] nextTable;//转移的时候用的数组
private transient volatile int sizeCtl;

我们可以发现相对于HashMap新加了一个sizeCtl,是一个非常重要的变量。它的意义可以解释为。

  • sizeCtl :
    • 默认为0,用来控制table的初始化和扩容操作
    • -1 代表table正在初始化
    • -N 表示有N-1个线程正在进行扩容操作
    • 其余情况:
      1. 如果table未初始化,表示table需要初始化的大小。
      2. 如果table初始化完成,表示table的扩容阈值,默认是table大小的0.75倍

希望在看源码前对基本的构成心中有大概的轮廓,看代码才会更加轻松
在这里插入图片描述

内部类

Node

static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;volatile V val;volatile Node<K,V> next;Node(int hash, K key, V val, Node<K,V> next) {this.hash = hash;this.key = key;this.val = val;this.next = next;}public final K getKey()       { return key; }public final V getValue()     { return val; }public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }public final String toString(){ return key + "=" + val; }public final V setValue(V value) {throw new UnsupportedOperationException();}public final boolean equals(Object o) {Object k, v, u; Map.Entry<?,?> e;return ((o instanceof Map.Entry) &&(k = (e = (Map.Entry<?,?>)o).getKey()) != null &&(v = e.getValue()) != null &&(k == key || k.equals(key)) &&(v == (u = val) || v.equals(u)));}/*** Virtualized support for map.get(); overridden in subclasses.*/Node<K,V> find(int h, Object k) {Node<K,V> e = this;if (k != null) {do {K ek;if (e.hash == h &&((ek = e.key) == k || (ek != null && k.equals(ek))))return e;} while ((e = e.next) != null);}return null;}}

相较于HashMap的Node实现,ConcurrentHashMap的Node需要在子类中实现setValue,并且实现了该节点往后节点值得寻找方法find。

TreeNode

static final class TreeNode<K,V> extends Node<K,V> {TreeNode<K,V> parent;  // red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;    // needed to unlink next upon deletionboolean red;TreeNode(int hash, K key, V val, Node<K,V> next,TreeNode<K,V> parent) {super(hash, key, val, next);this.parent = parent;}Node<K,V> find(int h, Object k) {return findTreeNode(h, k, null);}/*** Returns the TreeNode (or null if not found) for the given key* starting at given root.*/final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {if (k != null) {TreeNode<K,V> p = this;do  {int ph, dir; K pk; TreeNode<K,V> q;TreeNode<K,V> pl = p.left, pr = p.right;if ((ph = p.hash) > h)p = pl;else if (ph < h)p = pr;else if ((pk = p.key) == k || (pk != null && k.equals(pk)))return p;else if (pl == null)p = pr;else if (pr == null)p = pl;else if ((kc != null ||(kc = comparableClassFor(k)) != null) &&(dir = compareComparables(kc, k, pk)) != 0)p = (dir < 0) ? pl : pr;else if ((q = pr.findTreeNode(h, k, kc)) != null)return q;elsep = pl;} while (p != null);}return null;}}

与HashMap不同,HashMap的TreeNode继承于LinkedHashMap.Entry<K,V>,并且使用了上百行代码对其进行了完整实现。但在ConcurrentHashMap中的TreeNode只是继承于Node,并且重写了find方法。对于树的完整实现放到了哪里呢?在ConcurrentHashMap中放到了TreeBin中。

TreeBin

这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。由于代码过多我们只贴出来他的构造函数。

TreeBin(TreeNode<K,V> b) {super(TREEBIN, null, null, null);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);break;}}}}this.root = r;assert checkInvariants(root);}

我们可以看到和HashMap一样熟悉的建立红黑树的操作。

ForwardingNode

在执行transfer操作期间,作为桶位的头结点。

三个原子操作

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);}static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,Node<K,V> c, Node<K,V> v) {return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);}static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);}
  • tabAt():获得在i位置上的Node节点
  • casTabAt():利用CAS算法设置i位置上的Node节点。
  • setTabAt():设置节点位置的值。
    而casTabAt()与setTabAt()方法的区别:
    所以真正要进行有原子语义的写操作需要使用casTabAt()方法,setTabAt()是在锁定桶的状态下需要使用的方法,如此方法实现只是带保守性的一种写法而已。

讲解的主要方法

//无参构造
public ConcurrentHashMap(){}
//设定容量
public ConcurrentHashMap(int initialCapacity){}
//将map转化为CHM
public ConcurrentHashMap(Map<? extends K, ? extends V> m){}
//对table进行初始化
private final Node<K,V>[] initTable()
//增
public V put(K key, V value)
//在同一个节点的个数超过8个的时候,会调用treeifyBin方法来看看是扩容还是转化为一棵树
private final void treeifyBin(Node<K,V>[] tab, int index)
数组扩容的主要方法就是transfer方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)
//查
public V get(Object key)
//删除
public V remove(Object key)

另外在讲解主要方法时会顺便讲解其调用的方法。

初始化

private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;while ((tab = table) == null || tab.length == 0) {//table初始化的操作只能有一个线程执行,sizectl<0说明有另外一个线程正在初始化。那么本线程的初始化资格就让出去,不再执行后序的初始化操作。if ((sc = sizeCtl) < 0)Thread.yield(); // lost initialization race; just spin//CAS将cs改为-1,表示本线程正在初始化。else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {try {if ((tab = table) == null || tab.length == 0) {int n = (sc > 0) ? sc : DEFAULT_CAPACITY;@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = tab = nt;sc = n - (n >>> 2);//sizeCtl更新为容量的0.75}} finally {sizeCtl = sc;}break;}}return tab;}

对于ConcurrentHashMap来说,调用它的构造方法仅仅是设置了一些参数而已。而整个table的初始化是在向ConcurrentHashMap中插入元素的时候发生的。如调用put、computeIfAbsent、compute、merge等方法的时候,调用时机是检查table==null。
初始化方法主要应用了关键属性sizeCtl 如果这个值<0,表示其他线程正在进行初始化,就放弃这个操作。在这也可以看出ConcurrentHashMap的初始化只能由一个线程完成。如果获得了初始化权限,就用CAS方法将sizeCtl置为-1,防止其他线程进入。初始化数组后,将sizeCtl的值改为0.75*n。

构造函数

public ConcurrentHashMap() {}
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;}
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {this.sizeCtl = DEFAULT_CAPACITY;putAll(m);}

有了HashMap基础前两个好理解,不过第二个设定initialCapacity时,在最后将sizeCtl设置为cap。第三个将sizeCtl设置为初始容量。不用担心sizeCtl会在后序中进行改变。我们来看一下他调用的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);}

其中tryPresize

private final void tryPresize(int size) {//由于有容量2的次幂限制,将容量设为大于等于size的2的次幂值。与HashMap一样该功能由tableSizeFor实现。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;//若table还未初始化,则进行初始化。if (tab == null || (n = tab.length) == 0) {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;}}}//c没有达到扩容阈值或本身长度已经到达最大值,则不扩容,直接退出。else if (c <= sc || n >= MAXIMUM_CAPACITY)break;//初始化了,准备将值放到新的table中。else if (tab == table) {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);}}}

由于水平原因,笔者在这里遇到了个问题。
问题:31-39行代码永远不会执行。
解释:在6行对sc进行了赋值,进入while循环的都是sc>=0的情况。从while到执行sc<0之前的期间没有出现对sc重新赋值的情况,那么运行到sc<0的if判断时,一定为false。那么作者写这段代码的含义是什么?
下面讲解一下resizeStamp(n)函数,他执行之后,rs的二进制位表示中低16位为1,0-15位存储n二进制表示的最高非0位前0的个数值。不明白的看下面的博客讲解。
ConcurrentHashMap的源码分析-resizeStamp
结合上面的提示可以理解程序的执行流程,这里重点提一下40行的(rs << RESIZE_STAMP_SHIFT) + 2),CAS将SizeCtl的值变为(rs << RESIZE_STAMP_SHIFT) + 2),他的值一定为负数,因为rs的16位为1,在左移16位,那符号位为1,通过原子操作将SizeCtl变为相应的负数,并且创建一个两倍长度的新table,并赋值到nextTable上。下面我们只需要搞清楚一点如何将数据迁移到newtable中。
这是该类最难的transfer函数

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {int n = tab.length, stride;if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)stride = MIN_TRANSFER_STRIDE; // subdivide range//若nextTab==null则建立newtableif (nextTab == null) {            // initiatingtry {@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];//扩张1倍的空间。nextTab = nt;} catch (Throwable ex) {      // try to cope with OOMEsizeCtl = Integer.MAX_VALUE;return;}nextTable = nextTab;//老数据存在0-n-1,所以下一个索引为ntransferIndex = n;}int nextn = nextTab.length;ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);boolean advance = true;boolean finishing = false; // to ensure sweep before committing nextTab//玄幻处理,i表示当前处理的桶下标。bound表示分得区间的下界。for (int i = 0, bound = 0;;) {Node<K,V> f; int fh;//控制桶的序号while (advance) {int nextIndex, nextBound;if (--i >= bound || finishing)advance = false;else if ((nextIndex = transferIndex) <= 0) {i = -1;advance = false;}else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,nextBound = (nextIndex > stride ?nextIndex - stride : 0))) {bound = nextBound;i = nextIndex - 1;advance = false;}}//判断是否结束if (i < 0 || i >= n || i + n >= nextn) {int sc;if (finishing) {nextTable = null;table = nextTab;sizeCtl = (n << 1) - (n >>> 1);return;}if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)return;finishing = advance = true;i = n; // recheck before commit}}//当前桶位空。插入一个fwdelse 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;}}}}}}

transfer可以拆分为这几个部分

  • 根据CPU数划分table的步长:

    • 若为单核CPU,步长为table.length
    • 若为多核CPU,步长为max((n >>> 3) / NPC, MIN_TRANSFER_STRIDE)
  • 若传入的nextTab为null,创建nextTable

    • 将创建的nextTab赋给全局变量nextTable
    • 将用于控制扩容区间的分配transferIndex初始化为table.length
  • 开始循环处理区间内各个桶的迁移,由advance控制程序。

    • while循环对区间[bound, i]进行分配或调整(–i)(27-43)
    • 开始下面四个分支
      • i越界:判断当前区间的迁移是否结束;(45-59)
      • 当前桶位null,插入一个fwd
      • 当前桶已经迁移,跳过
      • 其他:迁移桶内数据
        • 链表(72-101)
        • 红黑树(103-137)
  • 27-43:控制区间

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

while循环求得本次这一次多线程处理的区间范围。第一次循环1,2判断语句都不成立,进入第三个,生成第一个区间[bound,i]其中bound为nextBound,nextBound在cas中被改为nextIndex-stride,所以第一次区间为[nextIndex-stride, nextIndex-1],更新。下一次循环进入第一个判断语句,得到当前处理的i后,退出while,最后一次循环进入第二条判断语句,从后往前遍历到头了。

  • 45-59:结束条件
// i<0时,已经完成扩容,暂不知道(i>=n || i+n>=nextn)的含义
if (i < 0 || i >= n || i + n >= nextn) {int sc;// finishing为true则结束if (finishing) {nextTable = null; // 置空table = nextTab; // 替换sizeCtl = (n << 1) - (n >>> 1); // sizeCtl设为0.75倍的table.lengthreturn;}// finishing为false,但是该线程的任务已经完成,sizeCtl中的线程数减1if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {// 判断sizeCtl是否回到刚开始扩容的状态// 是则说明所有用于迁移的线程都结束工作,否则直接返回if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)return;// finishing置为truefinishing = advance = true;// i置为table.length,全部扫描一遍,确定所有桶都已经完成迁移i = n; // recheck before commit}
}

当i<0时,当前线程已经完成了table最后一个区间,对finishing进行判断:

  • finishing为true:

    • 全局变量nextTable为空
    • 替换table
    • 设置sizeCtl
  • finishing为false:

    • 原子操作对sizeCtl中线程数-1
    • 判断是否所有线程都结束工作,否则返回,是则继续
    • finishing为true
    • i为table.length,下一个循环开始全局检查。
  • 72-101:迁移链表

// 上锁,与putVal()同步,f为当前桶的头节点
synchronized (f) {if (tabAt(tab, i) == f) {Node<K,V> ln, hn;// hash>0为链表,树节点的hash=-2if (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;}// 根据每个节点的hash值迁移,由于节点中的key是不可变的,需要创建新的节点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)// 头插法,创建新节点时把自己作为next节点传入// 最后链表的顺序将会颠倒(除了lastRun之后的)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); // 原子操作把fwd置入旧表表示已经迁移advance = true;}else if (f instanceof TreeBin) {...}}
}

与HashMap扩容一样,链表中要分为两支队伍,根据更高一位是0 or 1来分队伍。在这里用头插法,然后将两队挂到不同的对应索引下。第一个for循环用来找到并构建其中一个新链表的最后位置(因为第二个for中用到的是头插法构建新链表),也是后面for循环的终止条件。

  • 103-137:迁移红黑树
synchronized (f) {if (tabAt(tab, i) == f) {Node<K,V> ln, hn;if (fh >= 0) {...}// 节点为红黑树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);// 和链表相同的判断,与运算==0的放在低位if ((h & n) == 0) {if ((p.prev = loTail) == null)lo = p;elseloTail.next = p;loTail = p;++lc;} // 不是0的放在高位else {if ((p.prev = hiTail) == null)hi = p;elsehiTail.next = p;hiTail = p;++hc;}}// 如果树的节点数小于等于 6,那么转成链表,反之,创建一个新的树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;}}
}

前面和HashMap中的split一致,逻辑也一致,只是使用了原子操作将两条链表或树挂到对应位置。

插入

主函数

public V put(K key, V value) {return putVal(key, value, false);}
final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();int hash = spread(key.hashCode());int binCount = 0;for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;if (tab == null || (n = tab.length) == 0)tab = initTable();else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break;                   // no lock when adding to empty bin}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;}

注意:hash值进行了更新操作为 hashcode ^ (hashcode >>> 16)) & HASH_BITS
函数流程:

  • table未被初始化,初始化table
  • table已经初始化,但是对应桶为空,新建Node加入,跳出
  • table已经初始化,且相应桶不为空,但桶正在迁移,当前线程帮助迁移。
  • table已经初始化,且相应桶不为空,且该桶未在迁移,将键值对加入桶中。
    • 桶中是链表,遍历这个链表:
      • 若存在:根据onlyIfAbsent修改value,跳出
      • 若不存在创建新的node挂到尾部,跳出
    • 桶中是红黑树: 通过TreeNode中putTreeValue写入。
      上面函数没有讲过的有treeifBin(),addCount()。二者都包含了扩容操作,但是扩容的结果是不同的。
      将treeifyBin中使用tryPresize进行的扩容为P扩容。
      将addCount进行的扩容称为C扩容。

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) // 扩容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);if ((p.prev = tl) == null)hd = p;elsetl.next = p;tl = p;}setTabAt(tab, index, new TreeBin<K,V>(hd));}}}}
}

如果插入一个键值对后,链表长度大于等于 TREEIFY_THRESHOLD,就需要进行扩容或转化红黑树。treeifyBin() 是这一操作的入口,首先判断 table 容量是否小于 MIN_TREEIFY_CAPACITY,如果是则进行 P 扩容(由 tryPresize() 进行的,有别于下文中由 addCount() 进行的),否则进行红黑树转化。

addCount

private final void addCount(long x, int check) {// 计数部分,暂不考虑CounterCell[] as; long b, s;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))) {//CAS失败进入fulladdCount(x,false),否则fullAddCount(x,true)fullAddCount(x, uncontended);return;}if (check <= 1)return;s = sumCount();}// 判断是否C扩容if (check >= 0) { Node<K,V>[] tab, nt; int n, sc;// 满足三个条件则C扩容:1.size达到sizeCtl;2.table非空;3.table长度非最大值while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&(n = tab.length) < MAXIMUM_CAPACITY) {// 以下为C扩容,和tryPresize()中的一个分支完全一致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();}}
}

20行之前addCount通过CounterCell数组计算键值对个数,后面对check进行判断若其>=0,且

  1. 键值对数达到szieCtl
  2. table非空
  3. table长度并非最大值。
    进行C扩容。下面的结构与tryPresize中的一个分支完全一致。

计数

主要讲解的就是addCount前20行实现。
通过 size() 可以获得当前键值对的数量,它将 sumCount() 获得的 long 类型的值转化为 int 返回。
sumCount() 则计算 baseCount 字段与 counterCells 数组中所有非空元素的记录值的和。

// 未发生争用前都用它计数
private transient volatile long baseCount;
// 用于同步counterCells数组结构修改的乐观锁资源
private transient volatile int cellsBusy;
// 支持多个线程同时通过counterCells中的多个元素计数
private transient volatile CounterCell[] counterCells;
public int size() {long n = sumCount();return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n); 
}
// 计算 baseCount 字段与所有 counterCells 数组的非空元素的和
final long sumCount() {CounterCell[] as = counterCells; CounterCell a;long sum = baseCount;if (as != null) {for (int i = 0; i < as.length; ++i) {if ((a = as[i]) != null)sum += a.value;}}return sum;
}

CHM将每个桶中的键值对数量保存在baseCount和CounterCell 数组 counterCells 中。在什么地方对baseCount 和 counterCells进行赋值的呢?在addCount前20行。
addCount()中有两层if,第一层:

  • counterCells不为空:跳转到第二层if;
  • counterCells为空:CAS操作增加baseCount,成功结束if,失败进入二层if
    第二层if创建counterCells
  • counterCells==null || couterCells[线程hash]==null,调用fullAddCount(x,true)
  • couterCells[线程hash]!=null,CAS更改cellvalue值
    • 成功:结束
    • 失败:调用fullAddCount(x, false)【线程冲突】
private final void fullAddCount(long x, boolean wasUncontended) {int h;//线程Hash是否初始化if ((h = ThreadLocalRandom.getProbe()) == 0) {//初始化ThreadLocalRandom.localInit();      // force initializationh = ThreadLocalRandom.getProbe();//初始化后不会发生冲突wasUncontended = true;}boolean collide = false;                // True if last slot nonemptyfor (;;) {CounterCell[] as; CounterCell a; int n; long v;//counterCells是否可以用来计数,即判断他整体是否为空if ((as = counterCells) != null && (n = as.length) > 0) {//这个桶为空,初始化counterCellsif ((a = as[(n - 1) & h]) == null) {if (cellsBusy == 0) {            // Try to attach new Cell//初始化,记录这个桶内的元素数量为要新建的元素个数。CounterCell r = new CounterCell(x); // Optimistic create//尝试拿锁if (cellsBusy == 0 &&U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {boolean created = false;try {               // Recheck under lock//拿到锁了,将新建的counterCell元素放到指定位置。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;//无需扩容}//存在竞争,重置wasUncontended后,下一句就是rehash,然后重试else if (!wasUncontended)       // CAS already known to failwasUncontended = true;      // Continue after rehash//上面获取锁失败,说明有其他线程得到了锁,那么尝试将值加到BaseCount上else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))break;//counterCells是否扩容or其长度是否>=处理器数else if (counterCells != as || n >= NCPU)collide = false;            // At max size or stale//没有扩容,数组长度<处理器数量,collide为true,计划给CounterCells数组扩容。else if (!collide)collide = true;//尝试拿锁else if (cellsBusy == 0 &&U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {try {//给counterCells扩容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}//更改当前线程的hash值h = ThreadLocalRandom.advanceProbe(h);}//没有执行上面的if说明counterCells为null,初始化counterCellselse if (cellsBusy == 0 && counterCells == as &&U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {boolean init = false;try {                           // Initialize tableif (counterCells == as) {CounterCell[] rs = new CounterCell[2];//初始长度为2,最小的2次幂rs[h & 1] = new CounterCell(x);counterCells = rs;init = true;//初始化成功}} finally {//放锁cellsBusy = 0;}if (init)break;}//counterCells为空or长度为0,并且拿锁失败。//尝试将其加到basecount上else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))break;                          // Fall back on using base}}

如何理解流程呢?
他的主体就是for,循环中三个if

  1. if ((as = counterCells) != null && (n = as.length) > 0)counterCells非空情况
  2. else if (cellsBusy == 0 && counterCells == as && U.compareAndSwapInt(this, CELLSBUSY, 0, 1))counterCells为null
  3. else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))counterCells为空且获取锁失败
  4. h = ThreadLocalRandom.advanceProbe(h);重新生成hash
    三种情况,具体的每种情况的逻辑看具体的注释。

读取

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;}// 该桶已经被迁移,则交由节点find()方法查找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;}

通过查看注释十分好理解,在首点匹配,桶迁移情况(调用find寻找)匹配失败后,搜索所有节点。

Node<K,V> find(int h, Object k) {// loop to avoid arbitrarily deep recursion on forwarding nodesouter: for (Node<K,V>[] tab = nextTable;;) {Node<K,V> e; int n;if (k == null || tab == null || (n = tab.length) == 0 ||(e = tabAt(tab, (n - 1) & h)) == null)return null;for (;;) {int eh; K ek;if ((eh = e.hash) == h &&((ek = e.key) == k || (ek != null && k.equals(ek))))return e;if (eh < 0) {if (e instanceof ForwardingNode) {tab = ((ForwardingNode<K,V>)e).nextTable;continue outer;}elsereturn e.find(h, k);}if ((e = e.next) == null)return null;}}
}

参考

  1. ConcurrentHashMap源码分析(1.8)
  2. 源码级解读ConcurrentHashMap,妈妈再也不用担心我的学的学习
  3. ConcurrentHashMap底层原理剖析
  4. ConcurrentHashMap 1.8 源码分析
查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. WPF_15_格式化绑定的数据

    为了得到更人性化的外观&#xff0c;需要设计如何修剪数据列表和数据字段。 数据转换 在基本绑定中&#xff0c;信息从源到目标传递过程没有任何变化。但有时候希望将信息转换到更友好的内容再呈现到界面上。WPF提供了两个工具&#xff1a; 字符串格式化值转换器 单个属性 …...

    2024/4/7 4:40:09
  2. SystemUI中的PowerUI简析

    PowerUI powerUI是SystemUI显示电池相关信息的模块&#xff0c;包括低电量提醒&#xff0c;危急电量关机提醒&#xff0c;高温关机提醒&#xff0c;省电模式等的功能。 在powerUI.java 中主要是两个函数start()和onReveice() 启动流程 SystemUI启动时会加载众多功能模块&…...

    2024/4/14 15:56:58
  3. HashMap底层探究(一)

    1.HashMap 实现原理 HashMap 基于 Hash 算法实现的 当我们往Hashmap中put元素时&#xff0c;利用key的hashCode重新hash计算出当前对象的元素在数组中的下标存储时&#xff0c;如果出现hash值相同的key&#xff0c;此时有两种情况。(1)如果key相同&#xff0c;则覆盖原始值&a…...

    2024/5/2 0:08:01
  4. 【STM32F407】第11章 ThreadX NetXDUO之UDP客户端/服务器

    最新教程下载&#xff1a;ThreadX NetXDUO网络协议栈教程更新记录贴&#xff0c;前11章已经发布&#xff08;2022-01-03&#xff09; - uCOS & uCGUI & emWin & embOS & TouchGFX & ThreadX - 硬汉嵌入式论坛 - Powered by Discuz! 第11章 ThreadX …...

    2024/4/20 1:08:52
  5. 相关子查询 索引 事务 视图 存储过程 数据导入导出 远程访问数据库

    文章目录相关子查询索引事务视图存储过程数据得导入导出远程访问数据库相关子查询 -- 相关子查询&#xff1a;子查询不可以独立运行&#xff0c;并且先运行外查询再运行子查询 -- 优点&#xff1a;简单&#xff0c;功能强大 -- 缺点&#xff1a;理解较难-- 查询本部门最高工资…...

    2024/4/14 15:57:03
  6. 如何对接线上支付接口

    线上支付是很多独立的app以及网页&#xff0c;H5需要做的&#xff0c;那么到底应该如何接入线上支付呢&#xff1f; 首先&#xff0c;我们要确认&#xff0c;自己的支付场景是什么。 确认好支付场景&#xff0c;那么收单机构也要准备好&#xff0c;支付宝的收单只能是支付宝&am…...

    2024/4/14 15:57:03
  7. 【vue.js之夯实基础-7】Javascript模块化编程入门 之(自执行匿名函数,放大模式,宽放大模式, 模块内部调用全局变量 显式地将其他变量输入模块)

    附上阮一峰大神的入门三部曲 模块的写法 AMD规范 require.js的用法 下面是上面文章的摘录与要点&#xff0c;已经自己直接在chrome的控制台实例操作后的验证截图 模块化编程 背景 随着网站逐渐变成"互联网应用程序"&#xff0c;嵌入网页的Javascript代码越来越庞…...

    2024/4/16 22:09:28
  8. 操作系统复习总结

    操作系统复习总结 什么是os 从用户的角度看呢&#xff0c;os是用户和硬件系统的借口。 从资源管理角度看&#xff0c;os是资源的管理者&#xff0c;这句话我觉得是核心&#xff0c;os是计算机的资源管理者&#xff0c;管理计算机的很多资源。 从虚拟机角度看&#xff0c;os是一…...

    2024/4/14 15:57:39
  9. Chrome浏览器安装插件

    可以使用网站“扩展迷”&#xff1a;https://www.extfans.com/下载插件&#xff0c;几乎所有的Chrome应用商店中的插件都包括了&#xff0c;供大家免费下载&#xff01; 站内包括如何下载&#xff0c;以及安装教程。 推荐几个比较好用的插件 1、AdBlock&#xff0c;网页中最让…...

    2024/4/14 15:57:34
  10. Java简易系统监视器system-monitoring

    Java简易系统监视器system-monitoring&#xff1a;实时显示CPU使用率、内存使用率、笔记本电脑电池剩余电量、时间&#xff08;时、分、秒&#xff09;。创建系统托盘&#xff0c;设置系统托盘菜单&#xff0c;窗体置顶显示。通过jna调用dll文件读取电池数据。 目录 效果图 …...

    2024/4/19 22:55:24
  11. IDEA中git的使用(项目初始化、分支创建、合并等)

    前言 做为一个开发人员&#xff0c;对于代码管理工具大家肯定不会陌生&#xff0c;我们国内常用的代码管理工具有gitlub、gitee、coding等等&#xff0c;至于github属于国际性的&#xff0c;网站有些卡&#xff0c;但是资源丰富。我们公司项目目前使用的是自己服务器搭建的gitl…...

    2024/4/14 15:58:00
  12. Unity Editor模式下删除Prefabs里的组件

    需求很简单&#xff0c;就是在Editor模式下删除Prefabs中的组件&#xff0c;直接上代码 [MenuItem("Assets/MyTools/删除组件")]public static void RemoveComponent(){GameObject[] selections Selection.gameObjects;for(int i 0; i < selections.Length; i){…...

    2024/4/25 3:41:30
  13. 机房备用蓄电池组远程监测方案

    蓄电池组广泛应用于应急电源、光伏发电、楼宇备用电源、机房备用蓄电池组、基站备用电源中。根据实际的应用、选择合适的蓄电池组远程在线监测解决方案可以大大的降低运营成本。 本文介绍采用基于4G与5G无线网络通信技术&#xff0c;可以实时采集由2V、6V、12V单体电池组成的2…...

    2024/4/20 3:09:13
  14. Kafka安装与简单应用

    1.下载安装 1.下载地址 http://archive.apache.org/dist/kafka/0.8.1.1/kafka_2.10-0.8.1.1.tgz 2.解压安装 tar -xzf kafka_2.10-0.8.1.1.tar.gz 3.配置server.properties文件 cd /usr/local/kafka/config vim server.properties 修改属性为&#xff1a;zookeeper.connectlo…...

    2024/4/27 22:56:02
  15. iOS SDWebImage详细介绍

    在iOS的图片加载框架中&#xff0c;SDWebImage使用频率非常高。它支持从网络中下载且缓存图片&#xff0c;并设置图片到对应的UIImageView控件或者UIButton控件。在项目中使用SDWebImage来管理图片加载相关操作可以极大地提高开发效率&#xff0c;让我们更加专注于业务逻辑实现…...

    2024/4/18 10:05:09
  16. 【算法竞赛学习】数据分析达人赛1:用户情感可视化分析

    赛题背景 赛题以网络舆情分析为背景&#xff0c;要求选手根据用户的评论来对品牌的议题进行数据分析与可视化。通过这道赛题来引导常用的数据可视化图表&#xff0c;以及数据分析方法&#xff0c;对感兴趣的内容进行探索性数据分析。 赛题数据 数据源&#xff1a; earphone_…...

    2024/4/19 11:23:43
  17. 单例模式(Singleton Pattern)

    一、简述 单例模式是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式&#xff0c;它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类&#xff0c;该类负责创建自己的对象&#xff0c;同时确保只有单个对象被创建。这个类提供了一种访问其唯一对象…...

    2024/4/14 15:57:50
  18. 大聪明教你学Java | 程序员的专属新年对联

    前言 时间过得真快&#xff0c;转眼间还有13天就要过年了&#xff0c;这里先提前祝各位小伙伴新年快乐。今天给各位小伙伴整理了一些程序员的专属新年对联&#xff0c;没准有些小伙伴看到对联就会想起自己过去一年里的血泪史&#x1f602; 闲言少叙&#xff0c;直接上对联&…...

    2024/4/14 15:57:45
  19. DDD浅析

    DDD&#xff08;Domain Driven Design&#xff09;在04年由著名建模专家eric evans发表的他最具影响力的书籍:《domain-driven design –tackling complexity in the heart of software》(中文译名&#xff1a;领域驱动设计—软件核心复杂性应对之道)一书中提出了“领域驱动设计…...

    2024/4/14 15:57:29
  20. Hadoop | HDFS 学习笔记(三) | HDFS 读写流程 | NN、2NN、DN 工作机制 | FsImage与Edits的合并

    文章目录参考资料运行环境一、HDFS 体系结构二、HDFS 存储原理2.1 冗余数据保存2.2 数据存取策略2.2.1 数据存放2.2.2 数据读取2.3 数据错误与恢复2.3.1 NN出错2.3.2 DN出错2.3.3 数据出错三、HDFS 读写过程3.1 读流程过程与分析3.2 写流程3.2.1 过程与分析![在这里插入图片描述…...

    2024/4/7 4:39:57

最新文章

  1. Vitis HLS 学习笔记--AXI4 主接口

    目录 1. 简介 2. 认识MAXI 3. MAXI突发操作 3.1 全局/本地存储器 3.2 MAXI优势与特点 3.3 查看MAXI报告 3.3.1 HW Interfaces 3.3.2 M_AXI Burst Information 3.4 MAXI 资源消耗 4. 理解 Volatile 4.1 标准C/C中的 volatile 4.2 HLS 中的 volatile 5. 总结 1. 简介…...

    2024/5/2 2:07:46
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. 【Locust分布式压力测试】

    Locust分布式压力测试 https://docs.locust.io/en/stable/running-distributed.html Distributed load generation A single process running Locust can simulate a reasonably high throughput. For a simple test plan and small payloads it can make more than a thousan…...

    2024/5/1 5:19:39
  4. 磁盘管理与文件管理

    文章目录 一、磁盘结构二、MBR与磁盘分区分区的优势与缺点分区的方式文件系统分区工具挂载与解挂载 一、磁盘结构 1.硬盘结构 硬盘分类&#xff1a; 1.机械硬盘&#xff1a;靠磁头转动找数据 慢 便宜 2.固态硬盘&#xff1a;靠芯片去找数据 快 贵 硬盘的数据结构&#xff1a;…...

    2024/5/1 13:00:58
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/1 17:30:59
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/4/30 18:14:14
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/29 2:29:43
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/4/30 18:21:48
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/27 17:58:04
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/27 14:22:49
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/28 1:28:33
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/30 9:43:09
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/27 17:59:30
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/4/25 18:39:16
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/28 1:34:08
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/26 19:03:37
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/29 20:46:55
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/30 22:21:04
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/1 4:32:01
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/27 23:24:42
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/28 5:48:52
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/30 9:42:22
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/4/30 9:43:22
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/30 9:42:49
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57