基礎介紹
要對AtomicInteger有一個深入的認識,就必須要了解一下悲觀鎖和樂觀鎖。
cpu是時分復用的,也就是把cpu的時間片,分配給不同的線程進程輪流執行,
時間片與時間片之間,需要進行cpu切換,也就是會發生進程的切換。切換涉及到清空
寄存器,緩存數據。然后重新加載新的thread所需數據。
當一個線程被掛起時,加入到阻塞隊列,在一定的時間或條件下,在通過
notify(),notifyAll()喚醒回來。在某個資源不可用的時候,就將cpu讓出,
把當前等待線程切換為阻塞狀態。等到資源(比如一個共享數據)可用了,那么就將線程喚醒,
讓他進入runnable狀態等待cpu調度。這就是典型的悲觀鎖的實現。
但是,由于在進程掛起和恢復執行過程中存在著很大的開銷。當一個線程正在等待鎖時,
它不能做任何事,所以悲觀鎖有很大的缺點。舉個例子,如果一個線程需要某個資源,但是
這個資源的占用時間很短,當線程第一次搶占這個資源時,可能這個資源被占用,如果此時掛起
這個線程,可能立刻就發現資源可用,然后又需要花費很長的時間重新搶占鎖,時間代價
就會非常的高。
所以就有了樂觀鎖的概念,他的核心思路就是,每次不加鎖而是假設沒有沖突而去完成某項操作,
如果因為沖突失敗就重試,直到成功為止。在上面的例子中,某個線程可以不讓出cpu,而是一直
while循環,如果失敗就重試,直到成功為止。所以,當數據爭用不嚴重時,樂觀鎖效果更好。
比如我們要說的AtomicInteger底層同步CAS就是一種樂觀鎖思想的應用。
CAS就是Compare and Swap的意思,比較并操作。很多的cpu直接支持CAS指令。CAS是項
樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,
而其它線程都失敗,失敗的線程并不會被掛起,而是被告知這次競爭中失敗,并可以再次嘗試。
CAS有3個操作數,內存值V,預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,
將內存值V修改為B,否則什么都不做。
</br>
CAS操作
CAS通過調用JNI的代碼實現的。JNI:Java Native Interface為JAVA本地調用,允許java調用其他語言。而compareAndSwapInt就是借助C來調用CPU底層指令實現的。#lock類似的cpu指令.
AtomicInteger內部有一個變量UnSafe:
private static final Unsafe unsafe = Unsafe.getUnsafe();
Unsafe類是一個可以執行不安全、容易犯錯的操作的一個特殊類。雖然Unsafe類中所有方法都是public的,但是這個類只能在一些被信任的代碼中使用。其實CAS指的是sun.misc.Unsafe這個類中的一些方法的統稱。例如,Unsafe這個類中有compareAndSwapInt、compareAndSwapLong等方法。
public final native boolean compareAndSwapInt(Object o, long V, int E, int N);
??CAS的過程是:它包含了3個參數CAS(O,V,E,N)。O表示要更新的對象。V表示指明更新的對象中的哪個變量,E是進行比較的值,如果V==E,則將N賦值給V。
??第二個參數V(offset),其實要更新的對象里的字段相對于對象初始位置的內存偏移量。通俗一點就是在CAS(O,V,E,N)中,O是你要更新那個對象,
V就是我要通過這個偏移量找到這個對象中的value對象,來對他進行操作。也就是說,如果我把1這個數字屬性更新到2的話,需要這樣調用:
compareAndSwapInt(this, valueOffset, 1, 2);
valueOffset字段表示內存位置,可以在AtomicInteger對象中使用unsafe得到:
static {
try {
valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex); }
}
AtomicInteger內部使用變量value表示當前的整型值,這個整型變量還是volatile的,表示內存可見性,一個線程修改value之后保證對其他線程的可見性:
private volatile int value;
AtomicInteger內部還封裝了一下CAS,定義了一個compareAndSet方法,只需要2個參數:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
其中
unsafe.compareAndSwapInt(this, valueOffset, expect, update);
類似于:
if (this == expect) {
this = update;
return true;
} else {
return false;
}
具體函數說明
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe unsafe = Unsafe.getUnsafe(); //這里是初始化一個Unsafe對象。因為CAS是這個類中的方法。
private static final long valueOffset;
static {
try {
/*一個java對象可以看成是一段內存,各個字段都得按照一定的順序放在這段內存里,
同時考慮到對齊要求,可能這些字段不是連續放置的,用這個方法能準確地告訴你某個
字段(也就是下面的value字段)相對于對象的起始內存地址的字節偏移量,因為是相對
偏移量,所以它其實跟某個具體對象又沒什么太大關系,跟class的定義和虛擬機的內
存模型的實現細節更相關。通俗一點就是在CAS(O,V,E,N)中,O是你要更新那個對象,
V就是我要通過這個偏移量找到這個對象中的value對象,來對他進行操作。*/
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//volatile,保證變量的可見性。
private volatile int value;
//有參構造
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {
}
public final int get() {
return value;
}
public final int getAndIncrement() {
//為什么會無限循環,先得到當前的值value,然后再把當前的值加1
//加完之后使用cas原子操作讓當前值加1處理正確。當然cas原子操作不一定是成功的,
//所以做了一個死循環,當cas操作成功的時候返回數據。這里由于使用了cas原子操作,
//所以不會出現多線程處理錯誤的問題。
//比如,
// 1. Thread-A進入循環獲取current=1,然后切下cpu,Thread-B上cpu得到current=1再下cpu;
// 2. 然后Thread-A的next值為2,進行cas操作并且成功的時候,將value修改成了2;這時候內存中value值為2了,
// 這個時候Thread-B切上cpu執行的next值為2,當進行cas操作的時候由于expected值已經是2,而不是1了;所以cas操作會失敗,
// 3. 失敗了怎么辦,在下個循環中得到的current就變成了2;也就不會出現多線程處理問題了!
for (;;) {
int current = get(); //得到當前的值
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
CAS缺點
??CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題。
---ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操作。
??1. ABA問題。因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那么A-B-A 就會變成1A-2B-3A。
??2. 循環時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支持處理器提供的pause指令那么效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決于具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序沖突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。
??3. 只能保證一個共享變量的原子操作。當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合并成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合并一下ij=2a,然后用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象里來進行CAS操作。