前面咱們說到volatile
不保證原子性,解決辦法就是使用AtomicInteger
代替int
,可是爲何使用AtomicInteger
就能夠保證了原子性了,是由於AtomicInteger
實現的就是CAS思想
和Unsafe
的支持。java
AtomicInteger atomicInteger = new AtomicInteger(5);
atomicInteger.compareAndSet(5, 10);
複製代碼
CAS:即比較和交換(compareAndSet
),CAS
的思想比較簡單就是三個值:當前內存值V,舊的預期值A,和要更新的值B,當且僅當內存值V等於預期值A,纔將內存值修改成B,並返回true
,不然什麼都不作,返回false。下面就以atomicInteger.getAndIncrement();
分析一下AtomicInteger
使用的CAS
思想。this
使用AtomicIntegeratom
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
atomicInteger.compareAndSet(5, 10);
atomicInteger.getAndIncrement();
}
複製代碼
compareAndSet方法spa
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
複製代碼
unsafe.getAndAddInt(this, valueOffset, 1)
說明:線程
new
的atomicInteger
對象;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;
複製代碼
說明:code
上述代碼就是根據對象的內存地址獲取當前內存的值,注意的是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;
}
複製代碼
說明:rem
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,兩個線程都拷貝一份到本身的工做內存中,getIntVolatile(var1, var2)
拿到value
值5,這時線程A被掛起。getIntVolatile(var1, var2)
方法獲取到value
值5,線程B沒有被掛起,並執行compareAndSwapInt
方法比較內存值也爲5,成功修改內存值爲10。compareAndSwapInt
方法比較,發現本身手裏的值(5)和內存的值(10)不一致,說明該值已經被其它線程提早修改過了,那隻能從新來一遍了。value
值,由於變量value
被volatile
修飾,因此其它線程對它的修改,線程A老是可以看到,線程A繼續執行compareAndSwapInt
進行比較替換,直到成功。前面的代碼分析中咱們知道getAndAddInt
方法有一個do-while
循環,若是CAS
失敗,就會進行一直嘗試比較,若是很長時間都不成功,就會增長CPU
的開銷。因此CAS
的一個缺點就是循環時間長開銷大,因爲this
表示的是當前對象,因此,存在另一個缺點就是只能保證一個共享變量的原子操做。最重要的缺點就是ABA問題。get
前面分析CAS
思想的時候,咱們知道一個線程會先獲取Value
的值,比較和交換的時候再獲取內存的值和手裏的value
進行比較,說的是若是一致就表示沒有被其餘線程修改過,而後就執行本身的交換操做,可是,若是,一個線程修改了,而後另外還有一個線程又修改會原來的值,這個時候一比較仍是同樣的,這就是ABA問題。簡單講就是狸貓換太子。若是業務中不關心中間操做,只在意開始和結尾是否一致就可,就沒必要要解決ABA 問題。
在java.util.concurrent.atomic
包下存在一個AtomicReference
類,就是原子引用,CAS
比較的只是內存中的值,如今增長一個版本號,比較值的同時再比較版本後是否一致。使用AtomicStampedReference
帶時間戳的原子引用來解決ABA問題。
public static void main(String[] args) {
AtomicStampedReference<Integer> 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();
}
複製代碼