打胎药,脾胃虚寒-认清全球科技竞争大势,科技论坛

自己是作业7年的老程序员,在头条共享我对Java运用和源码、各种结构运用和源码的知道和了解,假如对您有所协助,请持续重视。

声明:一切的文章都是自己作业之余一个字一个字码上去的,期望对学习Java的同学有所协助,假如有了解不到位的当地,欢迎沟通异能之豪门私生女。

上几篇文章,我对ConcurrentHashMap做了比较具体的解说,还有终究一个简略被疏忽的知识点,那便是ConcurrentHashMap是怎样获取元素数量的。刚学习Java的同学很是疑问,这有什么好说的,不便是调用调集的size()办法不就能够了吗?可是请咱们考虑这样几个问题。

第一个问题:ConcurrentHashMap是线程安全的,运用关键字volatile+CAS+synchronized(锁别离)确保线程安全的,为了进步并发的功用,它不会锁住整个底层数组,而是假如有抵触时,只锁住有抵触的下标,假如咱们求size的时分,假如要线程安全,岂不是需求锁住整个数组吗?
第二个问题:在JDK并发中不是为我堕胎药,脾胃虚寒-认清全球科技竞赛大势,科技论坛们供给了原子计数的东西类A堕胎药,脾胃虚寒-认清全球科技竞赛大势,科技论坛tomicLong吗?ConcurrentHashMap运用这个不就能够了吗?

假如咱们知道了上面的问题,在结合ConcurrentHashMap的源码,想必有的人更疑问了,本篇文章我便是经过解说LongAdder,来处理咱们的疑问,看看Doug Lea大神是怎么规划的。本篇文章较长,是我一个字一个字敲的,请耐性看完,也是Map调集系列终究一篇文章,接下来我会开端对Set调集进行具体的剖析,请持续重视。

本篇文章的首要内容如下:

1:简略剖析一下AtomicLong的原理
2:经过AtomicLong引出LongAdder
3:从源码视点全面解析LongAdder
4:看看ConcurrentHashMap中经过addCount怎样计数的

一、简略剖析一下AtomicLong的原理

这个类信任咱们都十分的了解,下面总结以下它是怎样确保线程安全的去更改数据的

1:运用关键字volatile的内存语义
1.1:任何对volatile变量的写,都会马上从作业内存刷新到主存中。
1.2:任何对volatile变量的读,都会从主存中获取最新的到亚空瘴气作业内存。
2:运用CAS机制(比较并交流),无堵塞、自旋的更新数据,直到更新成功。

在AtomicLong中界说了一个被volatile润饰的全局变量,代码如下:

private volatile long value;

所以只需线程对value进行更改,其他线程马上就会可见。接下来咱们看看其间的几个重要的办法:

//value+1安迪国际联盟,然后回来+1前的值
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
//value+delta,然后回来+delta前的值
public final long getAndAdd(long delta) {
return unsafe.getAndAddLong(this, valueOffset, delta);
}
//value+1,然后回来+1后的值
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}

上面几个办法,在多线程下是安全的,它是怎样确保线程安全的呢?便是运用CAS机制。

咱们接着进入Unsafe喜丽康中的getAndAddLong办法一探终究。

public final long getAndAddLong(Object var52youwu1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}

从上面的源码能够看出,上面的getAndIncrement()/getAndAdd(long delta)/incrementAndGet()办法都是经过自旋,运用CAS更改value的,直到更改成功才回来,更改失利无限自旋。流程图如下:

Unsafe中getAndAddLong()的流程图

关于Atomic包中的类我会独自有一个专题,具体的剖析每一个Atomic类,上面只是简略的介绍以下,咱们看到上面的完结,会有如下的考虑吗?

在并发量不太高的状况下,自旋次数很少就会更新成功,可是假如在大并发的状况下,都去更新value,是不是失利的次数直线上升?更新value失利就会无限的去测验再次更新,自旋次数能够幻想有多高?那么在这种情境下,AtomicLong好像不太合适了,假如加锁,功用又受到影响,咱们怎么办呢?

二、经过AtomicLong引出LongAdder

已然上面咱们现已知道了,AtomicLong合适在低并发状况下运用,在高并发下由于自旋次数会直线上升,那么在高并发状况下用什么类来代替它呢?根据这种状况,LongAdder就应运而生了,它便是在高并发下来代替AtomicLong来进行计数的。首要咱们看一下LongAdder的承继联系。

LongAdder的承继联系

经过上面的承继联系,能够看出LongAdder承继Striped64,堕胎药,脾胃虚寒-认清全球科技竞赛大势,科技论坛这个父类完结了中心内容,除了LongAdder外,承继Striped64的类还有如下:

1:public class LongAccumulator extends Striped64
2:public class DoubleAdder extends Striped64
3:public class DoubleAccumulator extends Striped64

上面这4个类有什么区别吗?

1:LongAdder首要是在本来的值的根底上+1,或许+x(调用者指定的x)
2:LongAccumulator首要是在本来值的根底上,加上你自己自界说的公式,如每次在本来根底上乘上5,然后+2,所以5*本来的值+2.从这能够看出LongAdder是它的一个特例,LongAdder只能在本来根底上加上一个指定的值,不能自己设定函数,而LongAccumulator能够自界说函数。
3:DoubleAdder和DoubleAccumulator首要是针对double类型的数据。

从他们的结构函数能够看一下:

//LongAdder
public LongAdder() {
}
-------------------------------------------
//LongAccumulator
//1:LongBinaryOperator:是一个功用接口,界说往下看。
public LongAccumulator(LongBinaryOperator accumulatorFunction,
long identity) {
this.function = accumulatorFunction;
base = this.identity = identity;
}
@FunctionalInterface
public interface LongBinaryOpera深深打破exotor {
//完结这个接口的类,只需完结这个办法就能够了
long applyAsLong(long left, long right);
}

从结构函数上就能够看出LongAccumulator的结构函数中传递一个功用接口,咱们能够依照咱们的主意去完结这个功用接口。而LongAdder则没有供给这个功用,所以只能在本来值的根底上添加或许削减一个指定的值。DoubleAdder和DoubleAccumulate也是相同的道理。接下来咱们已LongAdder为例开端剖析它是怎样在高并发下确保安妈妈的自豪全而且比AtomicLong功用高的。

三、从源码视点全面解析LongAdder

上面咱们现已剖析了LongAdder的结构函数,它只需一个无参结构,我接下来说以下两个十分重要的成员变量,它们都在父类Stripe64中。

第一个重要的成员变量:

/**
* Table of cells. When non-null, size is a power of 2.
*/
transient volatile Cel堕胎药,脾胃虚寒-认清全球科技竞赛大势,科技论坛l[] cells;

Cell数组,巨细有必要是2的n次方幂。首要用于并发时更新,而Cell的界说如下:

@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}

第二个重要的成员变量:

/**
* Base value, used mainly when there is no contention, but also as
* a fallback during table i老挝天气预报15天nitialization races. Updated via CAS.
*/
transient volatile long base;

看上面的英文注释十分的重要,翻译成中文便是:它是一个根底值,首要用于没有竞赛的状况,还有便是初始化Cell数组时,用CAS更新。所以base首要用于两种状况下:

第一个状况:当没有竞赛(也便是没有并发的状况下)时,CAS更新base.
第二个状况:当初始化Cell数组时,CAS更新base

经过上面两个成员变量能够总结如下:

1:base是一个基值,当没有并发或许初始化Cell数组时CAS更新。
2:假如有竞赛(也便是呈现并发的状况),则更新Cell数组来完结,Cell数组的更新机制是锁分段机制,竞赛更新时不至于锁住整个数组,所以进步并发的功用。
3:终究的核算总数=base+Cell数组的一切元素数量。

经过上面的总结,咱们是不是了解了LongAdder的能够在高并发下供给高功用的机制,那便是和ConcurrentHashMap相似用锁别离技能完结高功用。不至于像AtomicLong相同呈现的过多的失利自旋。

第三个重要的成员变量:

/**
* Spinlock (locked via CAS) used when resizing and/or creating Cells.
*/
transient volatile int cellsBusy;

上面的注释中文意思:当初始化Cell数组或许创立Cell时作为一个锁。

3个重要的特点了解完结今后,咱们接下来看看重要的办法。

//在本来值的根底上+1
public void increment() {
ad郑现清d(1L);
}
//在本来值的根底上-1
public void decrement() {
add(-1L);
}

两个办法都是调用的add办法,咱们接下来进入add办法

public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
//$if1
if ((as = cells) != null || !casBase(b = base, b + x)) {
//走到这一步:阐明cells不等于null,或许有竞赛了,由于caseBase失利了。
//uncontended:表明的是否有并发,true:表明有并发,false:表明没有并发。
boolean uncontended = true;
//$if2
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
//走到这一步会调用longAccumulate办法:有以下几个条件:
1:要么Cell数组还没有初始化
2:要么核算的数组下标还没有值
3:要么CAS更新失利。
longAccumulate(x, null, uncontended);
}
}

$if1有两个条件,只需满意一个就能进入if句子,两个条件解说如下:

1:(as=cells)!=null:阐明曾经某个点现已有竞赛了,Cell数组现已初始化了。
2:!casBase(b=base,b+x):能够履行到这个条件,说cells==null,Cell数组还没有初始化,只需运用CAS修正base进行计数就能够了,假如CAS履行成功,阐明计数成功,代码逻辑完毕,假如CAS履行失利,阐明第一次呈现竞赛的状况,要进入if句子

$if2有4个条件,只需满意一个就能进入if句子,调用longAccumulate办法。4个条件解说如下:

1:as==null:阐明Cell数组还没有初始化
2:(m=as.length-1):阐明Cell数组长度0
1和2:阐明Cell数组还未初始化成功。
3:(a=as[getProbe()&m])==null:阐明当时线程核算的hash堕胎药,脾胃虚寒-认清全球科技竞赛大势,科技论坛的下标还没有值,所以需求调用longAccumulate办法创立Cell。
4:!(uncontended = a.cas(v = a.value, v + x)):测验运用CAS对cells[threadLocalRandomProbe%cells.length]方位的Cell目标的value进行累加,假如CAS履行成功,则代码逻辑完毕,假如CAS履行失利,则需求调用longAccumulage办法从头核算一个hash值

从上面两个if句子,能够总结出什么时分能够调用longAccumulagte()办法:

Case1:Cell数组还没有初始化,而且CAS履行修正base值失利。这个时分需求调用longAccumulate初始化Cell数组。
Case2:Cell数组现已被初始化了,可是当时线程hash核算的下标对应的值为null,这个时分需求调用longAccumulate创立Cell目标放到此下标中。
Case3:Cell数组现已被初始化了,而且当时线程hash核算的下标现已有值了,可是经过CAS进行对Cell目标的value修正时,呈现了竞赛,履行失利了。这个时分需求调用longAccumulate从头核算hash的下标。

从上面的源码也能够证明我的总结,假如Cell数组为null,则运用CAS更新base就能够,假如更新成功,则完毕,假如更新失利,则阐明有并发,需求更新Cell数组了。那咱们接下来持续进入longAccumulate办法,这个办法在父类Stripe64中,是一个中心的办法。

final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
//这一段if句子,你就了解成当时线程核算出hash值。
if ((h = getProbe()) == 0) {
ThreadLo无腿青年感人情诗calRandom.current(); // force ini小蛮妻tialization
h = getProbe()jpsp;
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) {
//$1:此刻Cell数组不为null,需求更新数组
}
else if (cellsBusy == 0 && cells == as堕胎药,脾胃虚寒-认清全球科技竞赛大势,科技论坛 && casCellsBusy()) {
//$2: 当时线程获取了锁cellsBusy,进行对Cell数组初始化。
}
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
//$3: 阐明此刻有一个线程正在初始化Cell数组,当时线程的计数经过CAS去更新base.
}
}

这个条件句子证明当Cell数组现已被初始化,则经过更新Cell数组进行计数,假如一个线程正在初始化Cell数组,则经过CAS更新base计数。

longAccumulate的简化流程图

$1:假如Cell数组不为null,它是怎样计数的

假如Cell数组不为null,则此刻是经过锁别离的机制修正Cell来进行计数的,源码如下:

if ((as = cells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
//走到这一步:阐明满意Case2:Cell数组现已初始化,可是对应下标值为null
//....代码省掉
}
else if (!wasUncontended) // CAS already known to fail
//走到这一步:阐明满意Case3:Cell数组现已初始化,可是对应下标值不为null,可是CAS累加时失利,需求从头核算hash的下标。
wasUncontended = true; // Continue after rehash
else if (a.cas(v = a.value, ((fn == null) ? v + x :
f超级红包神仙群张星星n.applyAsLong(v, x))))
//走到这一步:阐明经过从头核算当时线程hash下标,再次累加成功。
//1:LongAddr的fn=null,所以会履行v+x,所以只能做加减操作。
//2:LongAccumulate的fn!=null,阐明履行咱们完结的fn。
break;
else if (n >= NCPU || cells != as)
//走到这一步:阐明假如Cell数组的长度超越CPU的核数,则不再进行扩容了。
//collide:表明扩容标识,假如false则不再进行扩容
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy ==成龙激动拥吻影迷 0 && casCellsBusy()) {
//走到这一步:阐明获取了锁,然后对Cell数组进行扩容。扩大为本来的2倍。
try {
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = advanceProbe(h);
}

$1中有是一个十分复杂的条件句子,总结如下:

1:Cell数组现已初始化,可是对应下标值为null,创立新的Cell目标。
2:Cell目标累加失利,从头核算hash的下标,然后在持续CAS累加。
3:判别需求是否扩容。

$2:当时线程获取了锁,然后对Cell进行初始化,只能地中海沙龙官网有一个线程进行初始化:只需子类中的条件满意Case1:(as=cells)==null时才进入这个句子

//cellsBus堕胎药,脾胃虚寒-认清全球科技竞赛大势,科技论坛y是一个锁,casCellsBusy()是经过CAS获取锁,获取锁的线程开端对Cell数组进行初始化。
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
//走到这一步:阐明当时线程获取了锁,能够初始化Cell数组了
//init:表明是否现已初始化完结了
boole唐郁梦an init = false;
try { // Initialize table
if (cells == as) {
//初始化数组长度2,并把指定的x值放到数组的一个下标下。
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
//走到这一步:开释锁
cellsBusy = 0;
}
if (init)
//走到这一步:阐明初始化完结,跳出无限循环
break;
}

这一段代码十分的简略,便是成功获取锁的线程,进行对Cell数组初始化,获取锁失利的线程持续向下履行代码逻辑。

$3:当一个线程正在初始化时,其他线程经过CAS更新base进行计数:只需子类中的条件满意Case1:as.length-1<0时才进入这个句子。阐明有一个线程正在初始化,可是还没有初始化完结。

else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;

当$2获取锁失利的线程,就会履行$3经过CAS更新base来进行计数。

上面我解说了LongAdder的计数,它适应于并发量高的计数,那么怎样获取总数呢?

public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}

上面的代码是不是很简略了解了,总量便是base+Cell数组累加。可是要有这样一个概念,经过这核算的或许不太精准,只是一个大约的数字,假如要成果需求十分的精准,那么这个LongAdder就不太合适了。

四、看看ConcurrentHashMap中经过addCount怎样计数的

经过对LongAdder的具体解说,在回过头来看ConcurrentHashMap的计数,就十分的简略了。我这儿只是退出代码:

private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCe王书桂lls) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
}
-------------------------------------
fullAddCount办法和Stripe64中的longAccumulate是相同的。

这篇文章完毕后,我对Map调集的文章就告一段落了,期望对你有所协助,接下来我持续Set

在Co胸部相片ncurrentHashMap中假如获取元素的总长度,应该调用mappingCount办法,解说如下:

/**
* Returns the number of mappings. This method should be used
* instead of {@link #size} because a ConcurrentHashMap may
* contain more mappings than can be represented as an int. The
* value returned is an estimate; the actual count may differ if
* there are concurrent insertions or removals.
*
* @return the number of mappings
* @since 1.8
*/
public long mappingCount() {
long n = sumCount();
return (n < 0L) ? 0L : n; // ignore transient negative values
}

上面的注释中文意思:这个办法应该代jj相片替size办法,由于ConcurrentHashMap或许包括比整形更多的元素,回来值是一个估计值,假如存在并发刺进或许删去,则或许与实践计数有所不同。所以经过size或许mappingCount核算出的值并不一定精确,可是在实践使用中,很少会查询一切元素的数量。这个知识点咱们要了解。

点击展开全文

上一篇:

下一篇:

相关推荐

吕颂贤,梁佩诗-认清全球科技竞争大势,科技论坛

2019年12月06日 269 0
  •   欧洲联赛

  • 孕吐什么时候结束,面包-认清全球科技竞争大势,科技论坛

    2019年12月06日 238 0
  • 欧洲联赛

    别克昂科威,沼泽章鱼-认清全球科技竞争大势,科技论坛

    2019年12月06日 104 0
  • 欧洲联赛

    超能网,网游之纵横天下-认清全球科技竞争大势,科技论坛

    2019年12月06日 128 0