首页 > 其他分享 >并发-CAS[老的,有时间我重新整理一下]

并发-CAS[老的,有时间我重新整理一下]

时间:2022-12-29 10:03:30浏览次数:93  
标签:LongAdder 并发 CAS 更新 原子 重新整理 int 线程


并发-CAS[老的,有时间我重新整理一下]

文章是直接从我本地word笔记粘贴过来的,排版啥的可能有点乱,凑合看吧

(一)执行原理

synchronized 是一个原子操作,但是比较重量级的.(因为这个线程拿到锁以后,其它线程都必须等待. 如果做的事情耗时非常久,那么就大大降低效率)

CAS是无锁,是乐观锁机制,我的操作某个数据的时候,悲观锁是先抢到锁,乐观锁是先拿到数据(oldValue),然后这个线程做完事情运算完值以后得到一个(新值)newValue , 我把这个新的值写回去的时候,我先用自己存的oldValue,然后准备用新值去更改数据的时候,我把现在变量目前的值先和旧的值先比较一下.

1.如果是一样的,说明没有别的线程改过,我再把newValue值赋值给oldValue ,让oldValue =newValue
2.如果不是一样,说明别的线程改过了,此时再获取获取当前旧的值再进行操作,操作完了再进行对比,一直比到整个操作成功为止

如果一直操作不成功不会死循环,因为CPU执行速度很快,执行一条指令时间是0.6纳秒.

![](/i/ll/?i=img_convert/1143735c7061fb7aba7ccf5fd2a2f1f7.png#align=left&display=inline&height=336&margin=[object Object]&originHeight=403&originWidth=472&status=done&style=none&width=394)

实现原子操作还可以使用当前的处理器基本都支持CAS()的指令,只不过每个厂家所实现的算法并不一样,每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值(就是原来的值)A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值(修改前的值)A,则将地址上的值赋为新值B,否则不做任何操作。

CAS的基本思路就是,如果这个地址上的值和期望的值(修改前的值)相等(说明在当前操作期间没有别的线程修改这个数据),则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作,直到成功为止。

CAS是怎么实现线程的安全呢?语言层面不做处理,我们将其交给硬件—CPU和内存,利用CPU的多处理能力,实现硬件层面的阻塞,再加上volatile变量的特性即可实现基于原子操作的线程安全。

CAS操作是在硬件级别可以保障一定是原子操作的,同一时间只有一个线程可以去执行CAS操作,先比较再设置
两个线程同时执行cas操作的话,只有一个线程能执行成功,执行失败的线程会重新尝试执行cas操作.

只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

(二)API

案例代码 zjj_parent_f2b842a1-d4dc-c10e-548a-2bc5eb5d413d

原子更新基本类型

AtomicBoolean:原子更新布尔类型。
AtomicInteger:原子更新整型。
AtomicLong:原子更新长整型。

以上三个类提供的方法几乎一模一样

AtomicInteger.

| //获取当前最新值,

**public final int **get() {

**return **value;

}

//设置当前值,具备volatile效果,方法用final修饰是为了更进一步的保证线程安全。

**public final void **set(**int **newValue) {

value = newValue;

}

//最终会设置成newValue,使用该方法后可能导致其他线程在之后的一小段时间内可以获取到旧值,有点类似于延迟加载

**public final void **lazySet(**int **newValue) {

unsafe.putOrderedInt(**this**, valueOffset, newValue);

}

//设置新值并获取旧值,底层调用的是CAS操作即unsafe.compareAndSwapInt()方法

**public final int **getAndSet(**int **newValue) {

**return **unsafe.getAndSetInt(**this**, valueOffset, newValue);

}

//如果当前值为expect,则设置为update(当前值指的是value变量)

**public final boolean **compareAndSet(**int **expect, **int **update) {

**return **unsafe.compareAndSwapInt(**this**, valueOffset, expect, update);

}

//当前值加1返回旧值,底层CAS操作

**public final int **getAndIncrement() {

**return **unsafe.getAndAddInt(**this**, valueOffset, 1);

}

//当前值减1,返回旧值,底层CAS操作

**public final int **getAndDecrement() {

**return **unsafe.getAndAddInt(**this**, valueOffset, -1);

}

//当前值增加delta,返回旧值,底层CAS操作

**public final int **getAndAdd(**int **delta) {

**return **unsafe.getAndAddInt(**this**, valueOffset, delta);

}

//当前值加1,返回新值,底层CAS操作

**public final int **incrementAndGet() {

**return **unsafe.getAndAddInt(**this**, valueOffset, 1) + 1;

}

//当前值减1,返回新值,底层CAS操作

**public final int **decrementAndGet() {

**return **unsafe.getAndAddInt(**this**, valueOffset, -1) - 1;

}

//当前值增加delta,返回新值,底层CAS操作

**public final int **addAndGet(**int **delta) {

**return **unsafe.getAndAddInt(**this**, valueOffset, delta) + delta;

}

//省略一些不常用的方法…

static AtomicInteger ai = new AtomicInteger(10); 设置初始值是10

•int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。

•boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。

•int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
getAndDecrement() 原子性地使当前值递减1 注意,这里返回的是自增前的值。

•int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。

原子更新数组

AtomicIntegerArray:原子更新整型数组里的元素。
AtomicLongArray:原子更新长整型数组里的元素。
AtomicReferenceArray:原子更新引用类型数组里的元素。
AtomicIntegerArray类主要是提供原子的方式更新数组里的整型

AtomicIntegerArray

主要是提供原子的方式更新数组里的整型,其常用方法如下。
•int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
•boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。
需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。

更新引用类型

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类。

AtomicReference:原子更新引用类型。
AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)。

以上几个类提供的方法几乎一样

AtomicReference
原子更新引用类型。

AtomicStampedReference
AtomicStampedReference是利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA问题了。这就是AtomicStampedReference的解决方案。
AtomicMarkableReference跟AtomicStampedReference差不多, AtomicStampedReference是使用pair的int stamp作为计数器使用,AtomicMarkableReference的pair使用的是boolean mark。 AtomicStampedReference可能关心的是动过几次,AtomicMarkableReference关心的是有没有被人动过,方法都比较简单。

方法

| //构造方法, 传入引用和戳

**public **AtomicStampedReference(V initialRef, **int **initialStamp)

//返回引用

**public **V getReference()

//返回版本戳

**public int **getStamp()

//如果当前引用 等于 预期值并且 当前版本戳等于预期版本戳, 将更新新的引用和新的版本戳到内存

**public boolean **compareAndSet(V expectedReference,

V   newReference,

**int **expectedStamp,

**int **newStamp)

//如果当前引用 等于 预期引用, 将更新新的版本戳到内存

**public boolean **attemptStamp(V expectedReference, **int **newStamp)

//设置当前引用的新引用和版本戳

**public void **set(V newReference, **int **newStamp)

AtomicMarkableReference:

前面AtomicStampedReference类可以通过版本戳知道引用变量中途被更改了几次,有时候,我们并不关心引用变量更改了几次,只是单纯的关心是否被修改过,所以就有了AtomicMarkableReference.

AtomicMarkableReference和AtomicStampedReference的唯一区别就是不再用int标识引用,而是使用boolean变量——表示引用变量是否被更改过。

原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。

原子更新字段类

原子更新字段类 作用就是保证更新类的某个字段,比如实体类的某个属性,用的比较少,比较麻烦,还不如用AtomicReference更省事一点.

如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段更新。
要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段(属性)必须使用public volatile修饰符。

AtomicIntegerFieldUpdater:
原子更新整型的字段的更新器。
AtomicLongFieldUpdater:
原子更新长整型字段的更新器。
AtomicReferenceFieldUpdater:
原子更新引用类型里的字段。

(三)LongAdder

案例代码: zjj_parent_2019/09/20_ 9:03:34_ybxx79ll7qh0lmiube4xwbsnyi9puu

JDK1.8时,java.util.concurrent.atomic包中提供了一个新的原子类:LongAdder。

根据Oracle官方文档的介绍,LongAdder在高并发的场景下会比它的前辈————AtomicLong 具有更好的性能,但是代价是消耗更多的内存空间。

AtomicLong是利用了底层的CAS操作来提供并发性的,调用了Unsafe类的getAndAddLong方法,该方法是个native方法,它的逻辑是采用自旋的方式不断更新目标值,直到更新成功。

在并发量比较少的时候,线程冲突的概率比较小,自旋的次数不会很多。但是,高并发环境下,N个线程同时进行自旋操作,会出现大量失败并不断自旋的情况,在任意时刻,只会有一个线程成功 , 此时AtomicLong的自旋会成为瓶颈。

这就是LongAdder引入的初衷——解决高并发环境下AtomicLong的自旋瓶颈问题,高并发的时候比AtomicLong性能更好,但是要消耗更多的内存空间.

AtomicLong中有个内部变量value保存着实际的long值,所有的操作都是针对该变量进行。也就是说,高并发环境下,value变量其实是一个热点,也就是N个线程竞争一个热点。
![](/i/ll/?i=img_convert/ac9c07f49185ba5dd96b8e0bdd15d5d3.png#align=left&display=inline&height=19&margin=[object Object]&originHeight=31&originWidth=346&status=done&style=none&width=208)

LongAdder的基本思路就是**分散热点**,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。

这种做法和ConcurrentHashMap中的“分段锁”其实就是类似的思路。

LongAdder提供的API和AtomicLong比较接近,两者都能以原子的方式对long型变量进行增减。
但是AtomicLong提供的功能其实更丰富,尤其是addAndGetdecrementAndGetcompareAndSet这些方法。
addAndGetdecrementAndGet除了单纯的做自增自减外,还可以立即获取增减后的值,而LongAdder则需要做同步控制才能精确获取增减后的值。如果业务需求需要精确的控制计数,做计数比较,AtomicLong也更合适。

另外,从空间方面考虑,LongAdder其实是一种“空间换时间”的思想,从这一点来讲AtomicLong更适合。

总之,低并发、一般的业务场景下AtomicLong是足够了。如果并发量很多,存在大量写多读少的情况,那LongAdder可能更合适。适合的才是最好的,如果真出现了需要考虑到底用AtomicLong好还是LongAdder的业务场景,那么这样的讨论是没有意义的,因为这种情况下要么进行性能测试,以准确评估在当前业务场景下两者的性能,要么换个思路寻求其它解决方案。

LongAdder内部实现

对于LongAdder来说,内部有一个base变量,一个Cell[]数组。
base变量:非竞态条件下,直接累加到该变量上。
Cell[]数组:多线程竞态条件下,累加个各个线程自己的槽Cell[i]中。
![](/i/ll/?i=img_convert/e7a147d844353b04b82f0eba02a1f11c.png#align=left&display=inline&height=15&margin=[object Object]&originHeight=25&originWidth=422&status=done&style=none&width=253)
![](/i/ll/?i=img_convert/20f3aca983c2fcb8800d22eabdfa41ec.png#align=left&display=inline&height=16&margin=[object Object]&originHeight=26&originWidth=370&status=done&style=none&width=222)
所以,最终结果的计算应该是,这个方法只能保证最终一致性,要是想获取实时的还是得用AtomicLong,如果对数据准确度要求不高可以使用LongAdder(比如网站统计访问次数就没必要太精确到那么精确,只要大概范围就可以了.这就是LongAdder的应用场景)

![](/i/ll/?i=img_convert/63adefbf0910d2cbabdd0b2fcdab7950.png#align=left&display=inline&height=127&margin=[object Object]&originHeight=261&originWidth=553&status=done&style=none&width=269)
![](/i/ll/?i=img_convert/64f180fc898977cb1fbb279e2c551eb0.png#align=left&display=inline&height=33&margin=[object Object]&originHeight=55&originWidth=218&status=done&style=none&width=131)
在实际运用的时候,只有从未出现过并发冲突的时候,base基数才会使用到,一旦出现了并发冲突,之后所有的操作都只针对Cell[]数组中的单元Cell。
![](/i/ll/?i=img_convert/49417177c46a547f04ec3a2ab4fdfb3c.png#align=left&display=inline&height=123&margin=[object Object]&originHeight=233&originWidth=787&status=done&style=none&width=416)

而LongAdder最终结果的求和,并没有使用全局锁,返回值不是绝对准确的,因为调用这个方法时还有其他线程可能正在进行计数累加,所以只能得到某个时刻的近似值,这也就是LongAdder并不能完全替代LongAtomic的原因之一。

而且从测试情况来看,线程数越多,并发操作数越大,LongAdder的优势越大,线程数较小时,AtomicLong的性能还超过了LongAdder。

(四)jdk1.8其他新增

除了新引入LongAdder外,还有引入了它的三个兄弟类:LongAccumulator、DoubleAdder、DoubleAccumulator
LongAccumulator是LongAdder的增强版。LongAdder只能针对数值的进行加减运算,而LongAccumulator提供了自定义的函数操作。
通过LongBinaryOperator,可以自定义对入参的任意操作,并返回结果(LongBinaryOperator接收2个long作为参数,并返回1个long)。
LongAccumulator内部原理和LongAdder几乎完全一样。
DoubleAdder和DoubleAccumulator用于操作double原始类型。

(五)ABA问题

如果一个变量V初次读取时是A值,并且在赋值时检查到它仍然是A值,那么我们就能说它的值没有改变过吗?
如果在此期间,它的值曾经改成了B,后来又改回为A。那么CAS操作就会误以为它从来没有改变过。
这个称为CAS的“ABA”问题。
当然,在大部分情况下ABA问题并不会影响程序并发的正确性。

文档:[和笔记对接]Java CAS ABA问题发生的场…
链接:​​​http://note.youdao.com/noteshare?id=f6e567a6d133e9adf7780402401016c8​

解决ABA问题

解决aba问题 jdk提供了两个类 AtomicMarkableReference和 AtomicStampedReference两个原子类

两者差别是

AtomicMarkableReference看值有没有变化.
AtomicStampedReference可以告诉我们当前变化的次数,因为是所谓的计数器,而AtomicMarkableReference带的版本戳是boolean型


标签:LongAdder,并发,CAS,更新,原子,重新整理,int,线程
From: https://blog.51cto.com/u_14861909/5976389

相关文章