原子變數&&CAS演算法
進入今天的主題先看一段程式碼和程式碼的輸出結果
class AtomicDemo implements Runnable{
private int serialNumber=0;
public int getSerialNumber() {
return serialNumber++;
}
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getSerialNumber());
}
}
public class TestAutomic {
public static void main(String[] args) {
AtomicDemo atomicDemo=new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(atomicDemo).start();
}
}
}
可以看到在多執行緒的情況下serialNumber++操作出現了問題,分析下為什麼會出現這個問題?
主記憶體和工作記憶體的區別:
根據JMM的設計,系統存在一個主記憶體(Main Memory),Java中所有變數都儲存在主存中,對於所有執行緒都是共享的。每條執行緒都有自己的工作記憶體(Working Memory),工作記憶體中儲存的是主存中某些變數的拷貝,執行緒對所有變數的操作都是在工作記憶體中進行,執行緒之間無法相互直接訪問,變數傳遞均需要通過主存完成。
由於serialNumber++操作,會在記憶體中產生一個臨時的變數temp,首先將temp=serialNumber,然後serialNumber=serialNumber+1操作,最後serialNumber=1去更新主存的值;但是在多執行緒併發的情況下,執行緒2拿到的serialNumber的值可能是0,可能不是serialNumber=serialNumber+1操作後的值。由於原子性的操作是不可以分割的,但是++的操作把serialNumber++分割了,在多執行緒的情況下,所以出現了這種情況。在Volatile關鍵字記憶體可見的情況下,輸出的結果還是一樣的,因為Volatile關鍵字不能保證變數狀態的“原子性操作”,怎麼解決這個問題,先看2個概念。
CAS演算法:
CAS (Compare-And-Swap) 是一種硬體對併發的支援,針對多處理器 操作而設計的處理器中的一種特殊指令,用於管理對共享資料的並 發訪問。
CAS 是一種無鎖的非阻塞演算法的實現。
CAS 包含了 3 個運算元:
需要讀寫的記憶體值 V
進行比較的值 A
擬寫入的新值 B
當且僅當 V 的值等於 A 時,CAS 通過原子方式用新值 B 來更新 V 的 值,否則不會執行任何操作。
原子變數:
類的小工具包,支援在單個變數上解除鎖的執行緒安全程式設計。事實上,此包中的類可將 volatile 值、欄位和陣列元素的概念擴充套件到那些也提供原子條件更新操作的類。
類 AtomicBoolean、AtomicInteger、AtomicLong 和 AtomicReference 的例項各自提供對 相應型別單個變數的訪問和更新。每個類也為該型別提供適當的實用工具方法。
AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray類進一步擴充套件了原子操 作,對這些型別的陣列提供了支援。這些類在為其陣列元素提供 volatile 訪問語義方 面也引人注目,這對於普通陣列來說是不受支援的。
核心方法:boolean compareAndSet(expectedValue, updateValue)
java.util.concurrent.atomic包下提供了一些原子操作的常用類:
AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference AtomicIntegerArray、AtomicLongArray
AtomicMarkableReference
AtomicReferenceArray
AtomicStampedReference
看上圖的下方的左邊部分,V=0 拿到是主存的值,A=0是作為比較的值,B=1是在當前的執行緒1,V+1之後之後的值(通過B來更新主存的值),右邊部分 V=0 在多執行緒的情況下拿到的可能是0,A=1是線上程1(多執行緒的情況下),+1之後改之後的值,B=1是當前的執行緒2,V+1之後的值,V!=A,此時不能拿B去更新主存的值,說白了就是在多個執行緒的情況下,只有一個執行緒會執行成功。但是由於CAS 是一種無鎖的非阻塞演算法的實現,壓根不會放棄CPU給它的執行權,執行的效率非常的高,又會去執行一遍,取主存拿值,然後在計算更新值。這個要清楚的是CAS演算法是一種硬體對併發的支援(看概念)。
修改後的程式碼
class AtomicDemo implements Runnable{
//private int serialNumber=0;
private AtomicInteger atomicInteger=new AtomicInteger();
public int getSerialNumber() {
return atomicInteger.getAndIncrement(); //自增加1
}
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getSerialNumber());
}
}
public class TestAutomic {
public static void main(String[] args) {
AtomicDemo atomicDemo=new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(atomicDemo).start();
}
}
}
原子變數保證了資料的原子性,volatile關鍵字記憶體可見性。
說到最後,到底CAS演算法是怎麼實現的? 下篇部落格介紹。