目录

1.成员变量

2.基础方法

2.1 spread

2.2 tabAt

2.3 casTabAt

2.4 setTabAt

2.5 resizeStamp

2.6 tableSizeFor

3. 构造方法

4.put

5 putVal

6 initTable

7 addCount

8. transfer

9.helpTransfer

10.get

11.remove

12.replaceNode

13.TreeBin

13.1 属性

13.2 构造器

13.3 putTreeVal

13.4 find

总结


 

1.成员变量

//散列表数组的最大限制
private static final int MAXIMUM_CAPACITY = 1 << 30;//散列表默认值
private static final int DEFAULT_CAPACITY = 16;static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//并发级别:jdk7历史遗留问题,仅仅在初始化的时候使用到,并不是真正的代表并发级别
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;//负载因子,JDK1.8中 ConcurrentHashMap 是固定值
private static final float LOAD_FACTOR = 0.75f;//树化阈值,指定桶位 链表长度达到8的话,有可能发生树化操作。
static final int TREEIFY_THRESHOLD = 8;//红黑树转化为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;//联合TREEIFY_THRESHOLD控制桶位是否树化,只有当table数组长度达到64且 某个桶位 中的链表长度达到8,才会真正树化
static final int MIN_TREEIFY_CAPACITY = 64;//线程迁移数据最小步长,控制线程迁移任务最小区间一个值
private static final int MIN_TRANSFER_STRIDE = 16;//计算扩容时候生成的一个 标识戳
private static int RESIZE_STAMP_BITS = 16;//结果是65535 表示并发扩容最多线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;//扩容相关
private static  final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;//当node节点hash=-1 表示当前节点已经被迁移了  ,fwd节点
static final int MOVED     = -1; // hash for forwarding nodes//node hash=-2 表示当前节点已经树化 且 当前节点为treebin对象  ,代理操作红黑树
static final int TREEBIN   = -2; // hash for roots of treesstatic final int RESERVED  = -3; // hash for transient reservations
//转化成二进制实际上是 31个 1  可以将一个负数通过位移运算得到一个正数static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash//当前系统的cpu数量
static final int NCPU = Runtime.getRuntime().availableProcessors();//为了兼容7版本的chp保存的,核心代码并没有使用到
private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("segments", Segment[].class),new ObjectStreamField("segmentMask", Integer.TYPE),new ObjectStreamField("segmentShift", Integer.TYPE)};//散列表,长度一定是2次方数
transient volatile Node<K,V>[] table;//扩容过程中,会将扩容中的新table 赋值给nextTable 保持引用,扩容结束之后,这里会被设置为Null
private transient volatile Node<K,V>[] nextTable;//LongAdder 中的 baseCount 未发生竞争时 或者 当前LongAdder处于加锁状态时,增量累到到baseCount中
private transient volatile long baseCount;/*** sizeCtl < 0* 1. -1 表示当前table正在初始化(有线程在创建table数组),当前线程需要自旋等待..* 2.表示当前table数组正在进行扩容 ,高16位表示:扩容的标识戳   低16位表示:(1 + nThread) 当前参与并发扩容的线程数量** sizeCtl = 0,表示创建table数组时 使用DEFAULT_CAPACITY为大小** sizeCtl > 0** 1. 如果table未初始化,表示初始化大小* 2. 如果table已经初始化,表示下次扩容时的 触发条件(阈值)*/
private transient volatile int sizeCtl;/**** 扩容过程中,记录当前进度。所有线程都需要从transferIndex中分配区间任务,去执行自己的任务。*/
private transient volatile int transferIndex;/*** LongAdder中的cellsBuzy 0表示当前LongAdder对象无锁状态,1表示当前LongAdder对象加锁状态*/
private transient volatile int cellsBusy;/*** LongAdder中的cells数组,当baseCount发生竞争后,会创建cells数组,* 线程会通过计算hash值 取到 自己的cell ,将增量累加到指定cell中* 总数 = sum(cells) + baseCount*/
private transient volatile CounterCell[] counterCells;// Unsafe mechanics
private static final sun.misc.Unsafe U;
/**表示sizeCtl属性在ConcurrentHashMap中内存偏移地址*/
private static final long SIZECTL;
/**表示transferIndex属性在ConcurrentHashMap中内存偏移地址*/
private static final long TRANSFERINDEX;
/**表示baseCount属性在ConcurrentHashMap中内存偏移地址*/
private static final long BASECOUNT;
/**表示cellsBusy属性在ConcurrentHashMap中内存偏移地址*/
private static final long CELLSBUSY;
/**表示cellValue属性在CounterCell中内存偏移地址*/
private static final long CELLVALUE;
/**表示数组第一个元素的偏移地址*/
private static final long ABASE;
private static final int ASHIFT;static {try {U = sun.misc.Unsafe.getUnsafe();Class<?> k = ConcurrentHashMap.class;SIZECTL = U.objectFieldOffset(k.getDeclaredField("sizeCtl"));TRANSFERINDEX = U.objectFieldOffset(k.getDeclaredField("transferIndex"));BASECOUNT = U.objectFieldOffset(k.getDeclaredField("baseCount"));CELLSBUSY = U.objectFieldOffset(k.getDeclaredField("cellsBusy"));Class<?> ck = CounterCell.class;CELLVALUE = U.objectFieldOffset(ck.getDeclaredField("value"));Class<?> ak = Node[].class;ABASE = U.arrayBaseOffset(ak);//表示数组单元所占用空间大小,scale 表示Node[]数组中每一个单元所占用空间大小int scale = U.arrayIndexScale(ak);//1 0000 & 0 1111 = 0if ((scale & (scale - 1)) != 0)throw new Error("data type scale not a power of two");//numberOfLeadingZeros() 这个方法是返回当前数值转换为二进制后,从高位到低位开始统计,看有多少个0连续在一块。//8 => 1000 numberOfLeadingZeros(8) = 28//4 => 100 numberOfLeadingZeros(4) = 29//ASHIFT = 31 - 29 = 2 ??//ABASE + (5 << ASHIFT)ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);} catch (Exception e) {throw new Error(e);}}

2.基础方法

2.1 spread

获取hash值

高16位和低16位作异或运算,然后将结果转为正数。通过spread方法,可以让高位也能参与进寻址运算。

static final int spread(int h) {return (h ^ (h >>> 16)) & HASH_BITS;
}

2.2 tabAt

得到 i 桶位的头节点。

该方法获取对象中offset偏移地址对应的对象field的值。实际上这段代码的含义等价于tab[i],但是为什么不直接使用 tab[i]来计算呢?

getObjectVolatile,一旦看到 volatile 关键字,就表示可见性。因为对 volatile 写操作 happen-before 于 volatile 读操作,因此其他线程对 table 的修改均对 get 读取可见;

虽然 table 数组本身是增加了 volatile 属性,但是“volatile 的数组只针对数组的引用具有volatile 的语义,而不是它的元素”。 所以如果有其他线程对这个数组的元素进行写操作,那么当前线程来读的时候不一定能读到最新的值。出于性能考虑,Doug Lea 直接通过 Unsafe 类来对 table 进行操作。

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

2.3 casTabAt

cas设置当前节点为桶位的头节点

通过 CAS 方式替换 i 桶位的节点,c 表示期望值,v 表示要修改成的值。

修改成功返回 true,失败返回 false。

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

2.4 setTabAt

设置 i 桶位的值为 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);
}

2.5 resizeStamp

获得扩容标识戳

扩容标识戳标志的是 table 正在进行从 n => 2n 扩容。一个线程如果要帮助 table 进行扩容,必须拿到扩容标识戳,在扩容标识戳一致的情况下才能参与扩容。

static final int resizeStamp(int n) {return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

Integer.numberOfLeadingZeros 这个方法是返回无符号整数 n 最高位非 0 位前面的 0 的个数。

比如 10 的二进制是 0000 0000 0000 0000 0000 0000 0000 1010,那么这个方法返回的值就是 28。

根据 resizeStamp 的运算逻辑,我们来推演一下,假如 n=16,那么 resizeStamp(16)=32796转化为二进制是[0000 0000 0000 0000 1000 0000 0001 1100]

接着再来看,当第一个线程尝试进行扩容的时候,会执行下面这段代码:

U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2) rs 左移 16 位,相当于原本的二进制低位变成了高位 1000 0000 0001 1100 0000 0000 00000000

然后再+2 =1000 0000 0001 1100 0000 0000 0000 0000+10=1000 0000 0001 1100 0000 00000000 0010

高 16 位代表扩容的标记、低 16 位代表并行扩容的线程数

这样来存储有什么好处呢?

1,首先在 CHM 中是支持并发扩容的,也就是说如果当前的数组需要进行扩容操作,可以由多个线程来共同负责

2,可以保证每次扩容都生成唯一的生成戳,每次新的扩容,都有一个不同的 n,这个生成戳就是根据 n 来计算出来的一个数字,n 不同,这个数字也不同

第一个线程尝试扩容的时候,为什么是+2

因为 1 表示初始化,2 表示一个线程在执行扩容,而且对 sizeCtl 的操作都是基于位运算的,所以不会关心它本身的数值是多少,只关心它在二进制上的数值,而 sc + 1 会在低 16 位上加 1。

2.6 tableSizeFor

返回大于等于 c 的最小的 2 的次方数

    /*** Returns a power of two table size for the given desired capacity.* See Hackers Delight, sec 3.2* 返回>=c的最小的2的次方数* c=28* n=27 => 0b 11011* 11011 | 01101 => 11111* 11111 | 00111 => 11111* ....* => 11111 + 1 =100000 = 32*/
private static final int tableSizeFor(int c) {int n = c - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

3. 构造方法

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));/*** sizeCtl > 0* 当目前table未初始化时,sizeCtl表示初始化容量*/this.sizeCtl = cap;
}public ConcurrentHashMap(Map<? extends K, ? extends V> m) {this.sizeCtl = DEFAULT_CAPACITY;putAll(m);
}public ConcurrentHashMap(int initialCapacity, float loadFactor) {this(initialCapacity, loadFactor, 1);
}public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {//参数校验if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)throw new IllegalArgumentException();//如果初始容量小于并发级别,那就设置初始容量为并发级别if (initialCapacity < concurrencyLevel)   initialCapacity = concurrencyLevel;   //16/0.75 +1 = 22long size = (long)(1.0 + (long)initialCapacity / loadFactor);// 22 - > 32int cap = (size >= (long)MAXIMUM_CAPACITY) ?MAXIMUM_CAPACITY : tableSizeFor((int)size);/*** sizeCtl > 0* 当目前table未初始化时,sizeCtl表示初始化容量*/this.sizeCtl = cap;
}

4.put

public V put(K key, V value) {//如果key已经存在,是否覆盖,默认是falsereturn putVal(key, value, false);
}

5 putVal

final V putVal(K key, V value, boolean onlyIfAbsent) {//控制k 和 v 不能为nullif (key == null || value == null) throw new NullPointerException();//通过spread方法,可以让高位也能参与进寻址运算。int hash = spread(key.hashCode());//binCount表示当前k-v 封装成node后插入到指定桶位后,在桶位中的所属链表的下标位置//0 表示当前桶位为null,node可以直接放着//2 表示当前桶位已经可能是红黑树int binCount = 0;//tab 引用map对象的table//自旋for (Node<K,V>[] tab = table;;) {//f 表示桶位的头结点//n 表示散列表数组的长度//i 表示key通过寻址计算后,得到的桶位下标//fh 表示桶位头结点的hash值Node<K,V> f; int n, i, fh;//CASE1:成立,表示当前map中的table尚未初始化..if (tab == null || (n = tab.length) == 0)//最终当前线程都会获取到最新的map.table引用。tab = initTable();//CASE2:i 表示key使用路由寻址算法得到 key对应 table数组的下标位置,tabAt 获取指定桶位的头结点 felse if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//进入到CASE2代码块 前置条件 当前table数组i桶位是Null时。//使用CAS方式 设置 指定数组i桶位 为 new Node<K,V>(hash, key, value, null),并且期望值是null//cas操作成功 表示ok,直接break for循环即可//cas操作失败,表示在当前线程之前,有其它线程先你一步向指定i桶位设置值了。//当前线程只能再次自旋,去走其它逻辑。if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break;                   // no lock when adding to empty bin}//CASE3:前置条件,桶位的头结点一定不是null。//条件成立表示当前桶位的头结点 为 FWD结点,表示目前map正处于扩容过程中..else if ((fh = f.hash) == MOVED)//看到fwd节点后,当前节点有义务帮助当前map对象完成迁移数据的工作//帮助扩容tab = helpTransfer(tab, f);//CASE4:当前桶位 可能是 链表 也可能是 红黑树代理结点TreeBinelse {//当插入key存在时,会将旧值赋值给oldVal,返回给put方法调用处..V oldVal = null;//使用sync 加锁“头节点”,理论上是“头结点”synchronized (f) {//为什么又要对比一下,看看当前桶位的头节点 是否为 之前获取的头结点?//为了避免其它线程将该桶位的头结点修改掉,导致当前线程从sync 加锁 就有问题了。之后所有操作都不用在做了。if (tabAt(tab, i) == f) {//条件成立,说明咱们 加锁 的对象没有问题,可以进来造了!//条件成立,说明当前桶位就是普通链表桶位。if (fh >= 0) {//1.当前插入key与链表当中所有元素的key都不一致时,当前的插入操作是追加到链表的末尾,binCount表示链表长度//2.当前插入key与链表当中的某个元素的key一致时,当前插入操作可能就是替换了。binCount表示冲突位置(binCount - 1)binCount = 1;//迭代循环当前桶位的链表,e是每次循环处理节点。for (Node<K,V> e = f;; ++binCount) {//当前循环节点 keyK ek;//条件一:e.hash == hash 成立 表示循环的当前元素的hash值与插入节点的hash值一致,需要进一步判断//条件二:((ek = e.key) == key ||(ek != null && key.equals(ek)))//       成立:说明循环的当前节点与插入节点的key一致,发生冲突了if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {//将当前循环的元素的 值 赋值给oldValoldVal = e.val;if (!onlyIfAbsent)e.val = value;break;}//当前元素 与 插入元素的key不一致 时,会走下面程序。//1.更新循环处理节点为 当前节点的下一个节点//2.判断下一个节点是否为null,如果是null,说明当前节点已经是队尾了,插入数据需要追加到队尾节点的后面。Node<K,V> pred = e;if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key,value, null);break;}}}//前置条件,该桶位一定不是链表//条件成立,表示当前桶位是 红黑树代理结点TreeBinelse if (f instanceof TreeBin) {//p 表示红黑树中如果与你插入节点的key 有冲突节点的话 ,则putTreeVal 方法 会返回冲突节点的引用。Node<K,V> p;//强制设置binCount为2,因为binCount <= 1 时有其它含义,所以这里设置为了2 binCount = 2;//条件一:成立,说明当前插入节点的key与红黑树中的某个节点的key一致,冲突了if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {//将冲突节点的值 赋值给 oldValoldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}}//说明当前桶位不为null,可能是红黑树 也可能是链表if (binCount != 0) {//如果binCount>=8 表示处理的桶位一定是链表if (binCount >= TREEIFY_THRESHOLD)//调用转化链表为红黑树的方法treeifyBin(tab, i);//说明当前线程插入的数据key,与原有k-v发生冲突,需要将原数据v返回给调用者。if (oldVal != null)return oldVal;break;}}}//1.统计当前table一共有多少数据//2.判断是否达到扩容阈值标准,触发扩容。addCount(1L, binCount);return null;
}

 

6 initTable

数组初始化方法,这个方法比较简单,就是初始化一个合适大小的数组。

sizeCtl :这个标志是在 Node 数组初始化或者扩容的时候的一个控制位标识,负数代表正在进行初始化或者扩容操作。

-1 代表正在初始化

-N 代表有 N-1 个线程正在进行扩容操作,这里不是简单的理解成 n 个线程,sizeCtl 就是-N

0 标识 Node 数组还没有被初始化,正数代表初始化或者下一次扩容的大小

/*** Initializes table, using the size recorded in sizeCtl.*      * sizeCtl < 0*      * 1. -1 表示当前table正在初始化(有线程在创建table数组),当前线程需要自旋等待..*      * 2.表示当前table数组正在进行扩容 ,高16位表示:扩容的标识戳   低16位表示:(1 + nThread) 当前参与并发扩容的线程数量*      **      * sizeCtl = 0,表示创建table数组时 使用DEFAULT_CAPACITY为大小*      **      * sizeCtl > 0*      **      * 1. 如果table未初始化,表示初始化大小*      * 2. 如果table已经初始化,表示下次扩容时的 触发条件(阈值)*/
private final Node<K,V>[] initTable() {//tab 引用map.table//sc sizeCtl的临时值Node<K,V>[] tab; int sc;//自旋 条件:map.table 尚未初始化while ((tab = table) == null || tab.length == 0) {if ((sc = sizeCtl) < 0)//大概率就是-1,表示其它线程正在进行创建table的过程,当前线程没有竞争到初始化table的锁。Thread.yield(); // lost initialization race; just spin//1.sizeCtl = 0,表示创建table数组时 使用DEFAULT_CAPACITY为大小//2.如果table未初始化,表示初始化大小//3.如果table已经初始化,表示下次扩容时的 触发条件(阈值)else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {try {//这里为什么又要判断呢? 防止其它线程已经初始化完毕了,然后当前线程再次初始化..导致丢失数据。//条件成立,说明其它线程都没有进入过这个if块,当前线程就是具备初始化table权利了。if ((tab = table) == null || tab.length == 0) {//sc大于0 创建table时 使用 sc为指定大小,否则使用 16 默认值.int n = (sc > 0) ? sc : DEFAULT_CAPACITY;@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//最终赋值给 map.tabletable = tab = nt;//n >>> 2  => 等于 1/4 n     n - (1/4)n = 3/4 n => 0.75 * n//sc 0.75 n 表示下一次扩容时的触发条件。sc = n - (n >>> 2);}} finally {//1.如果当前线程是第一次创建map.table的线程话,sc表示的是 下一次扩容的阈值//2.表示当前线程 并不是第一次创建map.table的线程,当前线程进入到else if 块 时,将//sizeCtl 设置为了-1 ,那么这时需要将其修改为 进入时的值。sizeCtl = sc;}break;}}return tab;
}

7 addCount

当前 table 元素数量加 x(使用 LongAdder 实现),然后判断 table 是否需要扩容或者当前线程是否需要协助扩容。

private final void addCount(long x, int check) {//as 表示 LongAdder.cells//b 表示LongAdder.base//s 表示当前map.table中元素的数量CounterCell[] as; long b, s;//条件一:true->表示cells已经初始化了,当前线程应该去使用hash寻址找到合适的cell 去累加数据//       false->表示当前线程应该将数据累加到 base//条件二:false->表示写base成功,数据累加到base中了,当前竞争不激烈,不需要创建cells//       true->表示写base失败,与其他线程在base上发生了竞争,当前线程应该去尝试创建cells。if ((as = counterCells) != null ||!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {//有几种情况进入到if块中?//1.true->表示cells已经初始化了,当前线程应该去使用hash寻址找到合适的cell 去累加数据//2.true->表示写base失败,与其他线程在base上发生了竞争,当前线程应该去尝试创建cells。//a 表示当前线程hash寻址命中的cellCounterCell a;//v 表示当前线程写cell时的期望值long v;//m 表示当前cells数组的长度int m;//true -> 未竞争  false->发生竞争boolean uncontended = true;//条件一:as == null || (m = as.length - 1) < 0//true-> 表示当前线程是通过 写base竞争失败 然后进入的if块,就需要调用fullAddCount方法去扩容 或者 重试.. LongAdder.longAccumulate//条件二:a = as[ThreadLocalRandom.getProbe() & m]) == null   前置条件:cells已经初始化了//true->表示当前线程命中的cell表格是个空,需要当前线程进入fullAddCount方法去初始化 cell,放入当前位置.//条件三:!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)//      false->取反得到false,表示当前线程使用cas方式更新当前命中的cell成功//      true->取反得到true,表示当前线程使用cas方式更新当前命中的cell失败,需要进入fullAddCount进行重试 或者 扩容 cells。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);//考虑到fullAddCount里面的事情比较累,就让当前线程 不参与到 扩容相关的逻辑了,直接返回到调用点。return;}if (check <= 1)return;//获取当前散列表元素个数,这是一个期望值s = sumCount();}//表示一定是一个put操作调用的addCountif (check >= 0) {//tab 表示map.table//nt 表示map.nextTable//n 表示map.table数组的长度//sc 表示sizeCtl的临时值Node<K,V>[] tab, nt; int n, sc;/*** sizeCtl < 0* 1. -1 表示当前table正在初始化(有线程在创建table数组),当前线程需要自旋等待..* 2.表示当前table数组正在进行扩容 ,高16位表示:扩容的标识戳   低16位表示:(1 + nThread) 当前参与并发扩容的线程数量** sizeCtl = 0,表示创建table数组时 使用DEFAULT_CAPACITY为大小** sizeCtl > 0** 1. 如果table未初始化,表示初始化大小* 2. 如果table已经初始化,表示下次扩容时的 触发条件(阈值)*///自旋//条件一:s >= (long)(sc = sizeCtl)//       true-> 1.当前sizeCtl为一个负数 表示正在扩容中..//              2.当前sizeCtl是一个正数,表示扩容阈值//       false-> 表示当前table尚未达到扩容条件//条件二:(tab = table) != null//       恒成立 true//条件三:(n = tab.length) < MAXIMUM_CAPACITY//       true->当前table长度小于最大值限制,则可以进行扩容。while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&(n = tab.length) < MAXIMUM_CAPACITY) {//扩容批次唯一标识戳//16 -> 32 扩容 标识为:1000 0000 0001 1011int rs = resizeStamp(n);//条件成立:表示当前table正在扩容//         当前线程理论上应该协助table完成扩容if (sc < 0) {//条件一:(sc >>> RESIZE_STAMP_SHIFT) != rs//      true->说明当前线程获取到的扩容唯一标识戳 非 本批次扩容//      false->说明当前线程获取到的扩容唯一标识戳 是 本批次扩容//条件二: JDK1.8 中有bug jira已经提出来了 其实想表达的是 =  sc == (rs << 16 ) + 1//        true-> 表示扩容完毕,当前线程不需要再参与进来了//        false->扩容还在进行中,当前线程可以参与//条件三:JDK1.8 中有bug jira已经提出来了 其实想表达的是 = sc == (rs<<16) + MAX_RESIZERS//        true-> 表示当前参与并发扩容的线程达到了最大值 65535 - 1//        false->表示当前线程可以参与进来//条件四:(nt = nextTable) == null//        true->表示本次扩容结束//        false->扩容正在进行中if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||transferIndex <= 0)break;//前置条件:当前table正在执行扩容中.. 当前线程有机会参与进扩容。//条件成立:说明当前线程成功参与到扩容任务中,并且将sc低16位值加1,表示多了一个线程参与工作//条件失败:1.当前有很多线程都在此处尝试修改sizeCtl,有其它一个线程修改成功了,导致你的sc期望值与内存中的值不一致 修改失败//        2.transfer 任务内部的线程也修改了sizeCtl。if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))//协助扩容线程,持有nextTable参数transfer(tab, nt);}//1000 0000 0001 1011 0000 0000 0000 0000 +2 => 1000 0000 0001 1011 0000 0000 0000 0010//条件成立,说明当前线程是触发扩容的第一个线程,在transfer方法需要做一些扩容准备工作else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2))//触发扩容条件的线程 不持有nextTabletransfer(tab, null);s = sumCount();}}
}

8. transfer

ConcurrentHashMap 支持并发扩容,实现方式是,把 Node 数组进行拆分,让每个线程处理自己的区域,假设 table 数组总长度是 64,默认情况下,那么每个线程可以分到 16 个 bucket。然后每个线程处理的范围,按照倒序来做迁移。

通过 for 自循环处理每个槽位中的链表元素,默认 advace 为真,通过 CAS 设置 transferIndex属性值,并初始化 i 和 bound 值,i 指当前处理的槽位序号,bound 指需要处理的槽位边界,先处理槽位 31 的节点; (bound,i) =(16,31) 从 31 的位置往前推动。

每存在一个线程执行完扩容操作,就通过 cas 执行 sc-1。

接着判断(sc-2) !=resizeStamp(n) << RESIZE_STAMP_SHIFT ; 如果相等,表示当前为整个扩容操作的 最后一个线程,那么意味着整个扩容操作就结束了;如果不相等,说明还得继续。

这么做的目的,一方面是防止不同扩容之间出现相同的 sizeCtl,另外一方面,还可以避免sizeCtl 的 ABA 问题导致的扩容重叠的情况。

扩容图解 

 

 判断是否需要扩容,也就是当更新后的键值对总数 baseCount >= 阈值 sizeCtl 时,进行rehash,这里面会有两个逻辑。

  1. 如果当前正在处于扩容阶段,则当前线程会加入并且协助扩容。
  2. 如果当前没有在扩容,则直接触发扩容操作。

扩容操作的核心在于数据的转移,在单线程环境下数据的转移很简单,无非就是把旧数组中的数据迁移到新的数组。但是这在多线程环境下,在扩容的时候其他线程也可能正在添加元素,这时又触发了扩容怎么办?可能大家想到的第一个解决方案是加互斥锁,把转移过程锁住,虽然是可行的解决方案,但是会带来较大的性能开销。因为互斥锁会导致所有访问临界区的线程陷入到阻塞状态,持有锁的线程耗时越长,其他竞争线程就会一直被阻塞,导致吞吐量较低。而且还可能导致死锁。

而 ConcurrentHashMap 并没有直接加锁,而是采用 CAS 实现无锁的并发同步策略,最精华的部分是它可以利用多线程来进行协同扩容。

它把 Node 数组当作多个线程之间共享的任务队列,然后通过维护一个指针来划分每个线程锁负责的区间,每个线程通过区间逆向遍历来实现扩容,一个已经迁移完的bucket会被替换为一个ForwardingNode节点,标记当前bucket已经被其他线程迁移完了。接下来分析一下它的源码实现。

fwd:这个类是个标识类,用于指向新表用的,其他线程遇到这个类会主动跳过这个类,因为这个类要么就是扩容迁移正在进行,要么就是已经完成扩容迁移,也就是这个类要保证线程安全,再进行操作。

advance:这个变量是用于提示代码是否进行推进处理,也就是当前桶处理完,处理下一个桶的标识。

finishing:这个变量用于提示扩容是否结束用的。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {//n 表示扩容之前table数组的长度//stride 表示分配给线程任务的步长int n = tab.length, stride;//  stride 固定为 16if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)stride = MIN_TRANSFER_STRIDE; // subdivide range//条件成立:表示当前线程为触发本次扩容的线程,需要做一些扩容准备工作//条件不成立:表示当前线程是协助扩容的线程..if (nextTab == null) {            // initiatingtry {//创建了一个比扩容之前大一倍的table@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;//记录迁移数据整体位置的一个标记。index计数是从1开始计算的。transferIndex = n;}//表示新数组的长度int nextn = nextTab.length;//fwd 节点,当某个桶位数据处理完毕后,将此桶位设置为fwd节点,其它写线程 或读线程看到后,会有不同逻辑。ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);//推进标记boolean advance = true;//完成标记boolean finishing = false; // to ensure sweep before committing nextTab//i 表示分配给当前线程任务,执行到的桶位//bound 表示分配给当前线程任务的下界限制int i = 0, bound = 0;//自旋for (;;) {//f 桶位的头结点//fh 头结点的hashNode<K,V> f; int fh;/*** 1.给当前线程分配任务区间* 2.维护当前线程任务进度(i 表示当前处理的桶位)* 3.维护map对象全局范围内的进度*/while (advance) {//分配任务的开始下标//分配任务的结束下标int nextIndex, nextBound;//CASE1://条件一:--i >= bound//成立:表示当前线程的任务尚未完成,还有相应的区间的桶位要处理,--i 就让当前线程处理下一个 桶位.//不成立:表示当前线程任务已完成 或 者未分配if (--i >= bound || finishing)advance = false;//CASE2://前置条件:当前线程任务已完成 或 者未分配//条件成立:表示对象全局范围内的桶位都分配完毕了,没有区间可分配了,设置当前线程的i变量为-1 跳出循环后,执行退出迁移任务相关的程序//条件不成立:表示对象全局范围内的桶位尚未分配完毕,还有区间可分配else if ((nextIndex = transferIndex) <= 0) {i = -1;advance = false;}//CASE3://前置条件:1、当前线程需要分配任务区间  2.全局范围内还有桶位尚未迁移//条件成立:说明给当前线程分配任务成功//条件失败:说明分配给当前线程失败,应该是和其它线程发生了竞争吧else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,nextBound = (nextIndex > stride ?nextIndex - stride : 0))) {bound = nextBound;i = nextIndex - 1;advance = false;}}//CASE1://条件一:i < 0//成立:表示当前线程未分配到任务if (i < 0 || i >= n || i + n >= nextn) {//保存sizeCtl 的变量int sc;if (finishing) {nextTable = null;table = nextTab;sizeCtl = (n << 1) - (n >>> 1);return;}//条件成立:说明设置sizeCtl 低16位  -1 成功,当前线程可以正常退出if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {//1000 0000 0001 1011 0000 0000 0000 0000//条件成立:说明当前线程不是最后一个退出transfer任务的线程if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)//正常退出return;finishing = advance = true;i = n; // recheck before commit}}//前置条件:【CASE2~CASE4】 当前线程任务尚未处理完,正在进行中//CASE2://条件成立:说明当前桶位未存放数据,只需要将此处设置为fwd节点即可。else if ((f = tabAt(tab, i)) == null)advance = casTabAt(tab, i, null, fwd);//CASE3://条件成立:说明当前桶位已经迁移过了,当前线程不用再处理了,直接再次更新当前线程任务索引,再次处理下一个桶位 或者 其它操作else if ((fh = f.hash) == MOVED)advance = true; // already processed//CASE4://前置条件:当前桶位有数据,而且node节点 不是 fwd节点,说明这些数据需要迁移。else {//sync 加锁当前桶位的头结点synchronized (f) {//防止在你加锁头对象之前,当前桶位的头对象被其它写线程修改过,导致你目前加锁对象错误...if (tabAt(tab, i) == f) {//ln 表示低位链表引用//hn 表示高位链表引用Node<K,V> ln, hn;//条件成立:表示当前桶位是链表桶位if (fh >= 0) {//lastRun//可以获取出 当前链表 末尾连续高位不变的 nodeint 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;}}//条件成立:说明lastRun引用的链表为 低位链表,那么就让 ln 指向 低位链表if (runBit == 0) {ln = lastRun;hn = null;}//否则,说明lastRun引用的链表为 高位链表,就让 hn 指向 高位链表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;}//条件成立:表示当前桶位是 红黑树 代理结点TreeBinelse if (f instanceof TreeBin) {//转换头结点为 treeBin引用 tTreeBin<K,V> t = (TreeBin<K,V>)f;//低位双向链表 lo 指向低位链表的头  loTail 指向低位链表的尾巴TreeNode<K,V> lo = null, loTail = null;//高位双向链表 lo 指向高位链表的头  loTail 指向高位链表的尾巴TreeNode<K,V> hi = null, hiTail = null;//lc 表示低位链表元素数量//hc 表示高位链表元素数量int lc = 0, hc = 0;//迭代TreeBin中的双向链表,从头结点 至 尾节点for (Node<K,V> e = t.first; e != null; e = e.next) {// h 表示循环处理当前元素的 hashint h = e.hash;//使用当前节点 构建出来的 新的 TreeNodeTreeNode<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;//将低位链表尾指针指向 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;}}}}}
}

链表迁移原理

1)高低位原理分析

ConcurrentHashMap 在做链表迁移时,会用高低位来实现,这里有两个问题要分析一下

1,如何实现高低位链表的区分

假如有这样一个队列 

 

 第 14 个槽位插入新节点之后,链表元素个数已经达到了 8,且数组长度为 16,优先通过扩容来缓解链表过长的问题

假如当前线程正在处理槽位为 14 的节点,它是一个链表结构,在代码中,首先定义两个变量节点 ln 和 hn,实际就是 lowNode 和 HighNode,分别保存 hash 值的第 x 位为 0 和不等于0 的节点

通过 fn&n 可以把这个链表中的元素分为两类,A 类是 hash 值的第 X 位为 0,B 类是 hash 值的第 x 位为不等于 0(至于为什么要这么区分,稍后分析),并且通过 lastRun 记录最后要处理的节点。最终要达到的目的是,A 类的链表保持位置不动,B 类的链表为 14+16(扩容增加的长度)=30

把 14 槽位的链表单独伶出来,用蓝色表示 fn&n=0 的节点,假如链表的分类是这样

 

for (Node<K,V> p = f.next; p != null; p = p.next) {int b = p.hash & n;if (b != runBit) {runBit = b;lastRun = p;}
}

通过上面这段代码遍历,会记录 runBit 以及 lastRun,按照上面这个结构,那么 runBit 应该是蓝色节点,lastRun 应该是第 6 个节点接着,再通过这段代码进行遍历,生成 ln 链以及 hn 链

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

 

 接着,通过 CAS 操作,把 hn 链放在 i+n 也就是 14+16 的位置,ln 链保持原来的位置不动。并且设置当前节点为 fwd,表示已经被当前线程迁移完了。

setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);

迁移完成以后的数据分布如下

 

 2)为什么要做高低位的划分

要想了解这么设计的目的,我们需要从 ConcurrentHashMap 的根据下标获取对象的算法来看,在 putVal 方法中 1018 行:

(f = tabAt(tab, i = (n - 1) & hash)) == null

通过(n-1) & hash 来获得在 table 中的数组下标来获取节点数据,【&运算是二进制运算符,1& 1=1,其他都为 0】。

 

 

 

9.helpTransfer

如果对应的节点存在,判断这个节点的 hash 是不是等于 MOVED(-1),说明当前节点是ForwardingNode 节点,意味着有其他线程正在进行扩容,那么当前现在直接帮助它进行扩容,因此调用 helpTransfer方法。

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {//nextTab 引用的是 fwd.nextTable == map.nextTable 理论上是这样。//sc 保存map.sizeCtlNode<K,V>[] nextTab; int sc;//条件一:tab != null 恒成立 true//条件二:(f instanceof ForwardingNode) 恒成立 true//条件三:((ForwardingNode<K,V>)f).nextTable) != null 恒成立 trueif (tab != null && (f instanceof ForwardingNode) &&(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {//拿当前标的长度 获取 扩容标识戳   假设 16 -> 32 扩容:1000 0000 0001 1011int rs = resizeStamp(tab.length);//条件一:nextTab == nextTable//成立:表示当前扩容正在进行中//不成立:1.nextTable被设置为Null 了,扩容完毕后,会被设为Null//       2.再次出发扩容了...咱们拿到的nextTab 也已经过期了...//条件二:table == tab//成立:说明 扩容正在进行中,还未完成//不成立:说明扩容已经结束了,扩容结束之后,最后退出的线程 会设置 nextTable 为 table//条件三:(sc = sizeCtl) < 0//成立:说明扩容正在进行中//不成立:说明sizeCtl当前是一个大于0的数,此时代表下次扩容的阈值,当前扩容已经结束。while (nextTab == nextTable && table == tab &&(sc = sizeCtl) < 0) {//条件一:(sc >>> RESIZE_STAMP_SHIFT) != rs//      true->说明当前线程获取到的扩容唯一标识戳 非 本批次扩容//      false->说明当前线程获取到的扩容唯一标识戳 是 本批次扩容//条件二: JDK1.8 中有bug jira已经提出来了 其实想表达的是 =  sc == (rs << 16 ) + 1//        true-> 表示扩容完毕,当前线程不需要再参与进来了//        false->扩容还在进行中,当前线程可以参与//条件三:JDK1.8 中有bug jira已经提出来了 其实想表达的是 = sc == (rs<<16) + MAX_RESIZERS//        true-> 表示当前参与并发扩容的线程达到了最大值 65535 - 1//        false->表示当前线程可以参与进来//条件四:transferIndex <= 0//      true->说明map对象全局范围内的任务已经分配完了,当前线程进去也没活干..//      false->还有任务可以分配。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;
}

10.get

public V get(Object key) {//tab 引用map.table//e 当前元素//p 目标节点//n table数组长度//eh 当前元素hash//ek 当前元素keyNode<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;//扰动运算后得到 更散列的hash值int h = spread(key.hashCode());//条件一:(tab = table) != null//true->表示已经put过数据,并且map内部的table也已经初始化完毕//false->表示创建完map后,并没有put过数据,map内部的table是延迟初始化的,只有第一次写数据时会触发创建逻辑。//条件二:(n = tab.length) > 0 true->表示table已经初始化//条件三:(e = tabAt(tab, (n - 1) & h)) != null//true->当前key寻址的桶位 有值//false->当前key寻址的桶位中是null,是null直接返回nullif ((tab = table) != null && (n = tab.length) > 0 &&(e = tabAt(tab, (n - 1) & h)) != null) {//前置条件:当前桶位有数据//对比头结点hash与查询key的hash是否一致//条件成立:说明头结点与查询Key的hash值 完全一致if ((eh = e.hash) == h) {//完全比对 查询key 和 头结点的key//条件成立:说明头结点就是查询数据if ((ek = e.key) == key || (ek != null && key.equals(ek)))return e.val;}//条件成立://1.-1  fwd 说明当前table正在扩容,且当前查询的这个桶位的数据 已经被迁移走了//2.-2  TreeBin节点,需要使用TreeBin 提供的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;
}

11.remove

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

12.replaceNode

final V replaceNode(Object key, V value, Object cv) {//计算key经过扰动运算后的hashint hash = spread(key.hashCode());//自旋for (Node<K,V>[] tab = table;;) {//f表示桶位头结点//n表示当前table数组长度//i表示hash命中桶位下标//fh表示桶位头结点 hashNode<K,V> f; int n, i, fh;//CASE1://条件一:tab == null  true->表示当前map.table尚未初始化..  false->已经初始化//条件二:(n = tab.length) == 0  true->表示当前map.table尚未初始化..  false->已经初始化//条件三:(f = tabAt(tab, i = (n - 1) & hash)) == null true -> 表示命中桶位中为null,直接break, 会返回if (tab == null || (n = tab.length) == 0 ||(f = tabAt(tab, i = (n - 1) & hash)) == null)break;//CASE2://前置条件CASE2 ~ CASE3:当前桶位不是null//条件成立:说明当前table正在扩容中,当前是个写操作,所以当前线程需要协助table完成扩容。else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);//CASE3://前置条件CASE2 ~ CASE3:当前桶位不是null//当前桶位 可能是 "链表" 也可能 是  "红黑树" TreeBinelse {//保留替换之前的数据引用V oldVal = null;//校验标记boolean validated = false;//加锁当前桶位 头结点,加锁成功之后会进入 代码块。synchronized (f) {//判断sync加锁是否为当前桶位 头节点,防止其它线程,在当前线程加锁成功之前,修改过 桶位 的头结点。//条件成立:当前桶位头结点 仍然为f,其它线程没修改过。if (tabAt(tab, i) == f) {//条件成立:说明桶位 为 链表 或者 单个 nodeif (fh >= 0) {validated = true;//e 表示当前循环处理元素//pred 表示当前循环节点的上一个节点Node<K,V> e = f, pred = null;for (;;) {//当前节点keyK ek;//条件一:e.hash == hash true->说明当前节点的hash与查找节点hash一致//条件二:((ek = e.key) == key || (ek != null && key.equals(ek)))//if 条件成立,说明key 与查询的key完全一致。if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {//当前节点的valueV ev = e.val;//条件一:cv == null true->替换的值为null 那么就是一个删除操作//条件二:cv == ev || (ev != null && cv.equals(ev))  那么是一个替换操作if (cv == null || cv == ev ||(ev != null && cv.equals(ev))) {//删除 或者 替换//将当前节点的值 赋值给 oldVal 后续返回会用到oldVal = ev;//条件成立:说明当前是一个替换操作if (value != null)//直接替换e.val = value;//条件成立:说明当前节点非头结点else if (pred != null)//当前节点的上一个节点,指向当前节点的下一个节点。pred.next = e.next;else//说明当前节点即为 头结点,只需要将 桶位设置为头结点的下一个节点。setTabAt(tab, i, e.next);}break;}pred = e;if ((e = e.next) == null)break;}}//条件成立:TreeBin节点。else if (f instanceof TreeBin) {validated = true;//转换为实际类型 TreeBin tTreeBin<K,V> t = (TreeBin<K,V>)f;//r 表示 红黑树 根节点//p 表示 红黑树中查找到对应key 一致的nodeTreeNode<K,V> r, p;//条件一:(r = t.root) != null 理论上是成立//条件二:TreeNode.findTreeNode 以当前节点为入口,向下查找key(包括本身节点)//      true->说明查找到相应key 对应的node节点。会赋值给pif ((r = t.root) != null &&(p = r.findTreeNode(hash, key, null)) != null) {//保存p.val 到pvV pv = p.val;//条件一:cv == null  成立:不必对value,就做替换或者删除操作//条件二:cv == pv ||(pv != null && cv.equals(pv)) 成立:说明“对比值”与当前p节点的值 一致if (cv == null || cv == pv ||(pv != null && cv.equals(pv))) {//替换或者删除操作oldVal = pv;//条件成立:替换操作if (value != null)p.val = value;//删除操作else if (t.removeTreeNode(p))//这里没做判断,直接搞了...很疑惑setTabAt(tab, i, untreeify(t.first));}}}}}//当其他线程修改过桶位 头结点时,当前线程 sync 头结点 锁错对象时,validated 为false,会进入下次for 自旋if (validated) {if (oldVal != null) {//替换的值 为null,说明当前是一次删除操作,oldVal !=null 成立,说明删除成功,更新当前元素个数计数器。if (value == null)addCount(-1L, -1);return oldVal;}break;}}}return null;
}

13.TreeBin

13.1 属性

//红黑树 根节点 
TreeNode<K,V> root;
//链表的头节点
volatile TreeNode<K,V> first;
//等待者线程(当前lockState是读锁状态)
volatile Thread waiter;/*** 1.写锁状态 写是独占状态,以散列表来看,真正进入到TreeBin中的写线程 同一时刻 只有一个线程。 1* 2.读锁状态 读锁是共享,同一时刻可以有多个线程 同时进入到 TreeBin对象中获取数据。 每一个线程 都会给 lockStat + 4* 3.等待者状态(写线程在等待),当TreeBin中有读线程目前正在读取数据时,写线程无法修改数据,那么就将lockState的最低2位 设置为 0b 10*/
volatile int lockState;// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock

13.2 构造器

TreeBin(TreeNode<K,V> b) {//设置节点hash为-2 表示此节点是TreeBin节点super(TREEBIN, null, null, null);//使用first 引用 treeNode链表this.first = b;//r 红黑树的根节点引用TreeNode<K,V> r = null;//x表示遍历的当前节点for (TreeNode<K,V> x = b, next; x != null; x = next) {next = (TreeNode<K,V>)x.next;//强制设置当前插入节点的左右子树为nullx.left = x.right = null;//条件成立:说明当前红黑树 是一个空树,那么设置插入元素 为根节点if (r == null) {//根节点的父节点 一定为 nullx.parent = null;//颜色改为黑色x.red = false;//让r引用x所指向的对象。r = x;}else {//非第一次循环,都会来带else分支,此时红黑树已经有数据了//k 表示 插入节点的keyK k = x.key;//h 表示 插入节点的hashint h = x.hash;//kc 表示 插入节点key的class类型Class<?> kc = null;//p 表示 为查找插入节点的父节点的一个临时节点TreeNode<K,V> p = r;for (;;) {//dir (-1, 1)//-1 表示插入节点的hash值大于 当前p节点的hash//1 表示插入节点的hash值 小于 当前p节点的hash//ph p表示 为查找插入节点的父节点的一个临时节点的hashint dir, ph;//临时节点 keyK pk = p.key;//插入节点的hash值 小于 当前节点if ((ph = p.hash) > h)//插入节点可能需要插入到当前节点的左子节点 或者 继续在左子树上查找dir = -1;//插入节点的hash值 大于 当前节点else if (ph < h)//插入节点可能需要插入到当前节点的右子节点 或者 继续在右子树上查找dir = 1;//如果执行到 CASE3,说明当前插入节点的hash 与 当前节点的hash一致,会在case3 做出最终排序。最终//拿到的dir 一定不是0,(-1, 1)else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0)dir = tieBreakOrder(k, pk);//xp 想要表示的是 插入节点的 父节点TreeNode<K,V> xp = p;//条件成立:说明当前p节点 即为插入节点的父节点//条件不成立:说明p节点 底下还有层次,需要将p指向 p的左子节点 或者 右子节点,表示继续向下搜索。if ((p = (dir <= 0) ? p.left : p.right) == null) {//设置插入节点的父节点 为 当前节点x.parent = xp;//小于P节点,需要插入到P节点的左子节点if (dir <= 0)xp.left = x;//大于P节点,需要插入到P节点的右子节点elsexp.right = x;//插入节点后,红黑树性质 可能会被破坏,所以需要调用 平衡方法r = balanceInsertion(r, x);break;}}}}//将r 赋值给 TreeBin对象的 root引用。this.root = r;assert checkInvariants(root);
}

13.3 putTreeVal

final TreeNode<K,V> putTreeVal(int h, K k, V v) {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) {//当前循环节点xp 即为 x 节点的爸爸//x 表示插入节点//f 老的头结点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;
}

13.4 find

final Node<K,V> find(int h, Object k) {if (k != null) {//e 表示循环迭代的当前节点   迭代的是first引用的链表for (Node<K,V> e = first; e != null; ) {//s 保存的是lock临时状态//ek 链表当前节点 的keyint s; K ek;//(WAITER|WRITER) => 0010 | 0001 => 0011//lockState & 0011 != 0 条件成立:说明当前TreeBin 有等待者线程 或者 目前有写操作线程正在加锁if (((s = lockState) & (WAITER|WRITER)) != 0) {if (e.hash == h &&((ek = e.key) == k || (ek != null && k.equals(ek))))return e;e = e.next;}//前置条件:当前TreeBin中 等待者线程 或者 写线程 都没有//条件成立:说明添加读锁成功else if (U.compareAndSwapInt(this, LOCKSTATE, s,s + READER)) {TreeNode<K,V> r, p;try {//查询操作p = ((r = root) == null ? null :r.findTreeNode(h, k, null));} finally {//w 表示等待者线程Thread w;//U.getAndAddInt(this, LOCKSTATE, -READER) == (READER|WAITER)//1.当前线程查询红黑树结束,释放当前线程的读锁 就是让 lockstate 值 - 4//(READER|WAITER) = 0110 => 表示当前只有一个线程在读,且“有一个线程在等待”//当前读线程为 TreeBin中的最后一个读线程。//2.(w = waiter) != null 说明有一个写线程在等待读操作全部结束。if (U.getAndAddInt(this, LOCKSTATE, -READER) ==(READER|WAITER) && (w = waiter) != null)//使用unpark 让 写线程 恢复运行状态。LockSupport.unpark(w);}return p;}}}return null;
}

总结

在java8中,ConcurrentHashMap使用数组+链表+红黑树的组合方式,利用cas和synchronized保证并发写的安全。

引入红黑树的原因:链表查询的时间复杂度为On,但是红黑树的查询时间复杂度为O(log(n)),所以在节点比较多的情况下,使用红黑树可以大大提升性能。

链式桶是一个由node节点组成的链表。树状桶是一颗由TreeNode节点组成的红黑树。输的根节点为TreeBin类型。

当链表长度大于8整个hash表长度大于64的时候,就会转化为TreeBin。TreeBin作为根节点,其实就是红黑树对象。在ConcurrentHashMap的table数组中,存放的就是TreeBin对象,而不是TreeNoe对象。

数组table是懒加载的,只有第一次添加元素的时候才会初始化,所以initTable()存在线程安全问题。

重要的属性就是sizeCtl,用来控制table的初始化和扩容操作的过程:

● -1代表table正在初始化,其他线程直接join等待。

● -N代表有N-1个线程正在进行扩容操作,严格来说,当其为负数的时候,只用到了低16位,如果低16位为M,此时有M-1个线程进行扩容。

● 大于0有两种情况:如果table没有初始化,她就表示table初始化的大小,如果table初始化完了,就表示table的容量,默认是table大小的四分之三。

Transfer()扩容

table数据转移到nextTable。扩容操作的核心在于数据的转移,把旧数组中的数据迁移到新的数组。ConcurrentHashMap精华的部分是它可以利用多线程来进行协同扩容,简单来说,它把table数组当作多个线程之间共享的任务队列,然后通过维护一个指针来划分每个线程所负责的区间,每个线程通过区间逆向遍历来实现扩容,一个已经迁移完的 Bucket会被替换为一个Forwarding节点,标记当前Bucket已经被其他线程迁移完了。

helpTransfer()帮助扩容

ConcurrentHashMap并发添加元素时,如果正在扩容,其他线程会帮助扩容,也就是多线程扩容。

第一次添加元素时,默认初始长度为16,当往table中继续添加元素时,通过Hash值跟数组长度取余来决定放在数组的哪个Bucket位置,如果出现放在同一个位置,就优先以链表的形式存放,在同一个位置的个数达到了8个以上,如果数组的长度还小于64,就会扩容数组。如果数组的长度大于等于64,就会将该节点的链表转换成树。

通过扩容数组的方式来把这些节点分散开。然后将这些元素复制到扩容后的新数组中,同一个Bucket中的元素通过Hash值的数组长度位来重新确定位置,可能还是放在原来的位置,也可能放到新的位置。而且,在扩容完成之后,如果之前某个节点是树,但是现在该节点的“Key-Value对”数又小于等于6个,就会将该树转为链表。

put()

JDK1.8在使用CAS自旋完成桶的设置时,使用synchronized内置锁保证桶内并发操作的线程安全。尽管对同一个Map操作的线程争用会非常激烈,但是在同一个桶内的线程争用通常不会很激烈,所以使用CAS自旋、synchronized不会降低ConcurrentHashMap的性能。为什么不用ReentrantLock显式锁呢?如果为每 个桶都创建一个ReentrantLock实 例,就会带来大量的内存消耗,反过来,使用CAS自旋、synchronized,内存消耗的增加更小。

get()

get()通过UnSafe的getObjectVolatile()来读取数组中的元素。为什么要这样做?虽然HashEntry数组的引用是volatile类型,但是数组内元素的 用不是volatile类型,因此多线程对 数组元素的修改是不安全的,可能会在数组中读取到尚未构造完成的元素对象。get()方法通过UnSafe的getObjectVolatile方法来保证元素的读取安全,调用getObjectVolatile()去读取数组元素需要先获得元素在数组中的偏移量,在这里,get()方法根据哈希码计算出偏移量为u,然后通过偏移量u来尝试读取数值。

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

相关文章

  1. linux系统下文件的上传和下载(rz、sz)

    linux系统下的rz、sz上传和下载 rz 是从本地上传文件的时候用的&#xff0c; sz 相当于把Linux 服务器上的文件 下载到 本地。 通过这两个命令就可以把 Windows 和Linux 的文件 进行 互传了。 当然可以在 Windows 和Linux 互传文件有很多的方法&#xff0c; 比如 ftp ,samba…...

    2024/4/17 21:25:05
  2. 数据可视化大屏-1

    官方网站&#xff1a;https://echarts.apache.org/examples/zh/index.html#chart-type-bar 1.所使用的技术栈 ECharts 图标绘制、 Vue、Vue Router、VueX 、Webpack 前端框架开发 、 Axios 前后端数据的交互 即是前端向后端的获取、 WebSocket 前后端数据的推送 即是后端…...

    2024/4/7 1:15:28
  3. jupyter默认文件路径修改

    jupyter默认文件路径修改## 标题 创建存储位置—复制保存路径—cmd—jupyter notebook 路径 OK!...

    2024/4/20 6:12:07
  4. 关于图形验证码在本地启动有效 部署在linux环境时无效

    我的原因是由于调用了CaptchaService生成图形验证码&#xff0c;其中因为本地的jdk版本和linux上面安装的版本有一些区别 虽然都是1.8 本地带有的字体样式 linux服务器上面的jdk没有 所以就会造成是图片失效的效果 此时是报后端空指针异常 此时需要做的就是在linux中下载字体样…...

    2024/4/15 3:26:57
  5. mysql的or和and的组合使用,以及空和非空值的判断

    mysql的日常使用 1.mysql在查询数据设置条件时&#xff0c;如果需要判断字段的属性值不能为null或者等于null,大家的惯性思维就是 A!null 或者Anull,但是这样的写法实质上是查不出东西&#xff0c;写法是不对的&#xff0c;正确的写法应该是&#xff1a; where A is null 或…...

    2024/4/19 19:58:22
  6. cron表达式工具类

    cron表达式工具类MyCronUtil.class&#xff0c;java实现示例&#xff1a; package com.cwp.utils;/*** ClassName: MyCronUtil* Description: Cron表达式工具类* 目前支持三种常用的cron表达式* 1.每天的某个时间点执行 例:12 12 12 * * ?表示每天12时12分12秒执行* 2.每周的…...

    2024/4/19 10:13:30
  7. Replace函数

    update taskqueue set FileUrlReplace(FileUrl,,) where PlanID" update 表名 set 字段Replace(字段,需要替换的对应值,替换值) where PlanID"...

    2024/4/7 1:15:23
  8. windows + apache2.4 + flask+ python3.7 虚拟环境部署

    一、使用虚拟环境关键点 1、在虚拟环境Scripts中增加 activate_this.py 文件&#xff0c;3.7版本没有&#xff0c;3.5版本有&#xff0c;拷贝过去即可。 # activate_this.py # -*- coding: utf-8 -*- """Activate virtualenv for current interpreter:Use exe…...

    2024/4/19 22:52:05
  9. kuangbin动态规划

    文章目录Max Sum Plus PlusMax Sum Plus Plus 样例输入&#xff1a; 1 3 1 2 3 2 6 -1 4 -2 3 -2 3样例输出&#xff1a; 6 8代码模板&#xff1a; #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespac…...

    2024/4/19 12:34:37
  10. JavaScript-反转数组

    // 反转数组var arr [2, 0, 6, 1, 77, 0, 52, 0, 25, 7];var newArry [];for (var i arr.length - 1; i > 0; i--) {newArry[newArry.length] arr[i];}console.log(newArry);...

    2024/4/20 9:38:31
  11. es 聚合查询不要hits中的数据

    场景&#xff1a; 包含某个字段的所有数据根据企业ID进行分组&#xff0c;查出每个企业包含这个字段的数据有多少。我只想要聚合后的数据&#xff0c;不需要条件查出来的数据。我这里的处理方式就是使用from0&#xff0c;size0。这样hits中的数据就不会显示了。 案例&#xf…...

    2024/4/15 3:27:17
  12. SAP Spartacus UI TabParagraphContainerComponent 的工作原理

    首先渲染若干个 div button&#xff0c;个数等于 TabContainer 里包含的 Component 元素个数。 然后是利用 cxComponentWrapper 加载真实的 Component. integration lib 提供的 CMS mapping 没有生效。 但我看到已经配置进去了&#xff1f; 这不是调用了吗&#xff1f; 我查过…...

    2024/4/15 3:27:12
  13. windows + apache2.4 + python3.7 虚拟环境部署

    一、使用虚拟环境关键点 1、在虚拟环境Scripts中增加 activate_this.py 文件&#xff0c;3.7版本没有&#xff0c;3.5版本有&#xff0c;拷贝过去即可。 # activate_this.py # -*- coding: utf-8 -*- """Activate virtualenv for current interpreter:Use exe…...

    2024/4/19 19:33:31
  14. pig基本语法——输入输出存储查看结构

    参考官网地址:http://pig.apache.org/docs/r0.17.0/basic.html基础数据# cat /root/xytest/pig/data/demodataxiaoxiao,12,12.1aaa,13,1.1kjkj,12,12.1ddf,19,12.8常规的程序目前使用的都是pig -x local方式的A = load /root/xytest/pig/data/demodata using PigStorage(,) as…...

    2024/4/5 6:13:53
  15. Linux第三本书 第二章 linux中内核级加强型火墙的管理

    一.Selinux的功能 selinux对于文件的影响&#xff1a; 当selinux开启时,内核会对每个文件及每个开启的程序进行标签加载,标签内记录程序和文件的安全上下文(context)。 selinux对于程序功能的影响&#xff1a; 当selinux开启会对程序的功能加载开关,并设定此开关的状态为关闭&a…...

    2024/4/17 13:47:42
  16. Java 循环结构——文末福利相赠

    Java 循环结构 - for, while 及 do...while 顺序结构的程序语句只能被执行一次。 如果您想要同样的操作执行多次&#xff0c;就需要使用循环结构。 Java中有三种主要的循环结构&#xff1a; while 循环do…while 循环for 循环 在Java5中引入了一种主要用于数组的增强型for循…...

    2024/4/15 3:27:02
  17. 2021.11月

    1.你的程序要读入一行文本&#xff0c;其中以空格分隔为若干个单词&#xff0c;以.结束。你要输出每个单词的长度。这里的单词与语言无关&#xff0c;可以包括各种符号&#xff0c;比如its算一个单词&#xff0c;长度为4。注意&#xff0c;行中可能出现连续的空格&#xff1b;最…...

    2024/4/15 3:26:52
  18. qt之数据库连接删除

    qt之数据库连接删除 QSqlQuery类&#xff0c;提供了直接执行任意SQL语句并处理返回结果的方法。 QSqlDatabase&#xff1a;可以提供默认连接供Qt其他的SQL类使用。 1. 第一次.据库的连接 用到QSqlDatabase 以及其类中的:: database和 addDatabase&#xff0c;QString g_sThr…...

    2024/4/15 3:27:02
  19. php面试总结,需要自取

    算法 二分查找 归并排序 快排 回文数 整数反转 最短左前缀 LRU算法实现 两个有序数组合并为一个有序数组 求数组中子数组的最大和 数组中两数相加等于指定数的下标 BFS PHP 垃圾回收机制&#xff0c;循环引用问题如何解决的 …...

    2024/4/17 22:39:30
  20. 2021年中式面点师(中级)免费试题及中式面点师(中级)模拟试题

    题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 安全生产模拟考试一点通&#xff1a;2021年中式面点师&#xff08;中级&#xff09;免费试题为正在备考中式面点师&#xff08;中级&#xff09;操作证的学员准备的理论考试专题&#xff0c;每个月更新的中式面点师&a…...

    2024/4/15 3:27:02

最新文章

  1. Django中的实时通信:WebSockets与异步视图的结合

    &#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 在现代Web应用程序中&#xff0c;实时通信已经成为了必不可少的功能之一。无论是在线聊天、…...

    2024/4/20 9:41:53
  2. 梯度消失和梯度爆炸的一些处理方法

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

    2024/3/20 10:50:27
  3. CQT 质押者将从 Wormhole 的空投计划中获益,凸显跨链互操作的重要性

    在向去中心化协议治理迈进的战略进程中&#xff0c;Wormhole 近期公布了分发 6.17 亿枚 W 代币的计划&#xff0c;这一举措旨在激励多个链上社区参与。这些代币将促进在 Wormhole 平台内的治理参与度&#xff0c;并对贡献者进行激励&#xff0c;将近有 400,000 个钱包即将收到部…...

    2024/4/17 2:38:25
  4. Java插值查找知识点(含面试大厂题和源码)

    插值查找&#xff08;Interpolation Search&#xff09;是一种改进的二分查找算法&#xff0c;适用于数据分布均匀的有序数组。插值查找的基本思想是&#xff0c;根据要查找的关键字与数组的最大值和最小值之间的比例&#xff0c;预测关键字可能存在的位置&#xff0c;从而跳过…...

    2024/4/16 3:14:43
  5. 【外汇早评】美通胀数据走低,美元调整

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

    2024/4/19 14:24:02
  6. 【原油贵金属周评】原油多头拥挤,价格调整

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

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

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

    2024/4/19 11:57:31
  8. 【原油贵金属早评】库存继续增加,油价收跌

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

    2024/4/19 11:57:31
  9. 【外汇早评】日本央行会议纪要不改日元强势

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

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

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

    2024/4/19 11:57:53
  11. 【外汇早评】美欲与伊朗重谈协议

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

    2024/4/19 11:58:14
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

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

    2024/4/19 11:58:20
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

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

    2024/4/20 7:40:48
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

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

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

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

    2024/4/19 11:58:51
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

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

    2024/4/20 3:12:02
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

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

    2024/4/19 11:59:15
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

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

    2024/4/19 11:59:23
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

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

    2024/4/19 11:59:44
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

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

    2024/4/19 11:59:48
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

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

    2024/4/19 12:00:06
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

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

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

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

    2024/4/19 12:00:25
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

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

    2024/4/19 12:00:40
  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