死磕java并发cas_死磕Java——CAS
一、死磕Java——CAS
前面我們說到volatile不保證原子性,解決辦法就是使用AtomicInteger代替int,但是為什么使用AtomicInteger就可以保證了原子性了,是因為AtomicInteger實現的就是CAS思想和Unsafe的支持。
1.1.CAS是什么
AtomicInteger atomicInteger = new AtomicInteger(5);
atomicInteger.compareAndSet(5, 10);
CAS:即比較和交換(compareAndSet),CAS的思想比較簡單就是三個值:當前內存值V,舊的預期值A,和要更新的值B,當且僅當內存值V等于預期值A,才將內存值修改為B,并返回true,否則什么都不做,返回false。下面就以atomicInteger.getAndIncrement();分析一下AtomicInteger使用的CAS思想。
1.2.CAS的原理
使用AtomicInteger
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
atomicInteger.compareAndSet(5, 10);
atomicInteger.getAndIncrement();
}
compareAndSet方法
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
unsafe.getAndAddInt(this, valueOffset, 1)說明:
this:代表當前對象,這里就是外部new的atomicInteger對象;
valueOffset:代表當前對象的內存地址。
1:就是加1。
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
說明:
上述代碼就是根據對象的內存地址獲取當前內存的值,注意的是private volatile int value;添加了volatile關鍵字,所以,保證了可見性。
unsafe.getAndAddInt方法
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
說明:
unsafe類是CAS的核心類,由于Java無法直接操作底層系統,只能通過native修飾的本地方法操作,基于unsafe類就可以直接操作內存中的數據。
getAndAddInt(Object var1, long var2, int var4)中:Object var1:當前對象,long var2:當前對象的內存地址,int var4:需要更新的值,這里就是1。
var5 = this.getIntVolatile(var1, var2);:就是取到當前對象的內存值;
假如現在存在兩個線程,跑在不同的cpu上,內存中atomicInteger的原始值為5,兩個線程都拷貝一份到自己的工作內存中,
線程A通過getIntVolatile(var1, var2)拿到value值5,這時線程A被掛起。
線程B也通過getIntVolatile(var1, var2)方法獲取到value值5,線程B沒有被掛起,并執行compareAndSwapInt方法比較內存值也為5,成功修改內存值為10。
這時線程A恢復,執行compareAndSwapInt方法比較,發現自己手里的值(5)和內存的值(10)不一致,說明該值已經被其它線程提前修改過了,那只能重新來一遍了。
重新獲取value值,因為變量value被volatile修飾,所以其它線程對它的修改,線程A總是能夠看到,線程A繼續執行compareAndSwapInt進行比較替換,直到成功。
1.3.CAS的缺點
前面的代碼分析中我們知道getAndAddInt方法有一個do-while循環,如果CAS失敗,就會進行一直嘗試比較,如果很長時間都不成功,就會增加CPU的開銷。所以CAS的一個缺點就是循環時間長開銷大,由于this表示的是當前對象,所以,存在另外一個缺點就是只能保證一個共享變量的原子操作。最重要的缺點就是ABA問題。
1.3.1.什么是ABA問題
前面分析CAS思想的時候,我們知道一個線程會先獲取Value的值,比較和交換的時候再獲取內存的值和手里的value進行比較,說的是如果一致就表示沒有被其他線程修改過,然后就執行自己的交換操作,但是,如果,一個線程修改了,然后另外還有一個線程又修改會原來的值,這個時候一比較還是一樣的,這就是ABA問題。簡單講就是貍貓換太子。如果業務中不關心中間操作,只在乎開始和結尾是否一致就可,就不必要解決ABA 問題。
1.3.2.使用原子引用和版本機制解決ABA問題
在java.util.concurrent.atomic包下存在一個AtomicReference類,就是原子引用,CAS比較的只是內存中的值,現在增加一個版本號,比較值的同時再比較版本后是否一致。使用AtomicStampedReference帶時間戳的原子引用來解決ABA問題。
public static void main(String[] args) {
AtomicStampedReference reference = new AtomicStampedReference<>(10, 1);
new Thread(() -> {
int stamp = reference.getStamp();
System.out.println("T1拿到的第一次的版本號:" + stamp);
// 先暫停1秒,等T2線程拿到相同的初始版本號
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
reference.compareAndSet(10, 101, reference.getStamp(), reference.getStamp() + 1);
System.out.println("T1線程第一次操作后的版本號為:" + reference.getStamp());
reference.compareAndSet(101, 10, reference.getStamp(), reference.getStamp() + 1);
System.out.println("T1線程第二次操作后的版本號為:" + reference.getStamp());
}, "T1").start();
new Thread(() -> {
int stamp = reference.getStamp();
System.out.println("T2拿到的第一次的版本號:" + stamp);
// 先暫停3秒,等T1線程有充分的時候做一次ABA操作
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = reference.compareAndSet(10, 2019, stamp, stamp + 1);
System.out.println("當前內存中的最新值為:" + reference.getReference());
System.out.println("T2線程在T1線程執行完ABA問題后在執行的結果為:" + b);
}, "T2").start();
}
總結
以上是生活随笔為你收集整理的死磕java并发cas_死磕Java——CAS的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 荣耀新机通过国家 3C 质量认证,预计为
- 下一篇: 28岁戴牙套有用吗