JAVA併發編程-3-原子操作CAS和原子類

上一章:看這裏JAVA併發編程-2-線程併發工具類

一、CAS原理

1、爲什麼要有CAS

java中提供了sychronized和ReentrantLock的鎖機制,這是一種阻塞同步,可以理解是爲一種悲觀鎖的併發策略,總是認爲只要不去做正確的同步措施,就肯定會出現問題,無論共享數據是否真的會出現競爭,都要進行加鎖,需要進行線程等待喚醒等操作,性能消耗是很大的。

隨着 硬件指令集 的發展,我們有了另外一個選擇:基於衝突檢測的樂觀併發策略。通俗的講就是先操作,如果沒有其他線程競爭共享數據,那操作就成功了;如果共享數據有爭用,產生了衝突,那就採取其他補償措施(最常見的補償措施就是不斷重試,直到成功爲止),這種樂觀的併發策略不需要把線程掛起,是一種非阻塞同步

2、通過硬件指令集來保證

上面這段話我們特別標註了 硬件指令集 ,CAS(Compare-and-Swap)就是通過硬件指令來保證的,硬件保證一個從語義上看起來需要多次的操作只通過一條指令就能完成。這類常用的指令有:

  • 測試並設置(Test-and-Set)
  • 獲取並添加(Fetch-and-Increment)
  • 交換(Swap)
  • 比較並交換(Compare-and-Swap)

Compare-and-Swap是現代計算機新增的指令,java從JDK1.5開始支持cas指令,通過sun.misc.Unsafe類裏面的方法包裝提供,虛擬機在內部對方法做了特殊處理,即使編譯的結果就是一條平臺相關的處理器CAS指令,沒有方法調用的過程,或者可以任務被無條件內聯進去了。

3、什麼是CAS

CAS是怎樣的一個過程呢?

需要有3個操作數:
內存位置(可以理解爲變量的內存地址) V
舊的預期值 A
新值 B

假設一個線程要更改內存位置V的值,會先去改地址值拿到原值A,隨後將值改爲B,並同步會內存位置V。在將新值同步回內存時,發生一次CAS,先比較(Comapre)現在地址V的值是不是舊的預期值A,如果是,就將新值B賦值到地址V處(swap),否則不會執行更新,但是無論是否更新了V值,都會返回V的舊值,舊值不符合預期就進行補償策略,即在發生一次上述過程。

二、CAS的問題

1、ABA問題

CAS從語義上來說就不盡完美,存在這樣一個邏輯漏洞:如果一個變量V初次讀取時是A值,後面準備賦值時它仍然是A值,那麼我們就能說它的值沒有被其他線程改變過嗎?如果這段時間內它的值被改成了B,後來又被回了A,那CAS就誤認爲他從來沒有改變過,這個就是CAS的ABA問題。

JUC包爲了解決這個問題提供了一個帶標記的原子應用類“AtomicStampedReference”,可以通過控制變量值的版本來保證CAS的正確性。目前來看這個類比較雞肋,大部分情況下ABA問題並不會影響程序併發的正確性,如果需要解決ABA問題,改用鎖的方式獲取會更好。

2、開銷問題

如果極端情況下,CAS操作預期值總是不符合預期,那麼cpu會不斷循環重試,開銷也存在一定問題。

3、只能保證一個共享變量的原子操作

如果需要同時修改多個變量,一個CAS恐怕不夠。
java中提供了AtomicReference等引用類型可以使用。

三、原子類的使用

1、AtomicInteger

要注意辨析
public final int getAndIncrement() //得到原值後自增
public final int incrementAndGet() //自增後得到值

public class UseAtomicInt {
	
	static AtomicInteger ai = new AtomicInteger(10);
	
    public static void main(String[] args) {
    	System.out.println(ai.getAndIncrement());//10--->11
    	System.out.println(ai.incrementAndGet());//11--->12--->out
    	System.out.println(ai.get());
    }
}

2、AtomicReference

引用類型的原子操作類 要特別注意一個問題☝️,即並不會改變原實體類的引用。
下面代碼的11、12行打印新對象信息,13、14行打印原對象的信息

public class UseAtomicReference {
	
	static AtomicReference<UserInfo> userRef = new AtomicReference<UserInfo>();
	
    public static void main(String[] args) {
        UserInfo user = new UserInfo("Mark", 15);//要修改的實體的實例
        userRef.set(user);
        
        UserInfo updateUser = new UserInfo("Bill", 17);//要變化的新實例
        userRef.compareAndSet(user, updateUser);
        System.out.println(userRef.get().getName());
        System.out.println(userRef.get().getAge());
        System.out.println(user.getName());
        System.out.println(user.getAge());        
    }
    
    //定義一個實體類
    static class UserInfo {
        private String name;
        private int age;
        public UserInfo(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public String getName() {
            return name;
        }
        public int getAge() {
            return age;
        }
    }
}

3、AtomicStampedReference

這個類上面提到過,是用來解決ABA問題的,即在構造方法
public AtomicStampedReference(V initialRef, int initialStamp) ;
可以通過initialStamp參數定義一個版本號。

public class useAtomicStampedReference {
    static AtomicStampedReference<String> asr =
            new AtomicStampedReference<>("Mark", 0);


    public static void main(String[] args) throws InterruptedException {
        final int oldStamp = asr.getStamp();//那初始的版本號
        final String oldReferenc = asr.getReference();

        System.out.println(oldReferenc + "===========" + oldStamp);

        Thread rightStampThread = new Thread(() -> System.out.println(Thread.currentThread().getName()
                + "當前變量值:" + oldReferenc + "當前版本戳:" + oldStamp + "-"
                + asr.compareAndSet(oldReferenc, oldReferenc + "Java", oldStamp, oldStamp + 1)));

        Thread errorStampThread = new Thread(() -> {
            String reference = asr.getReference();
            System.out.println(Thread.currentThread().getName()
                    + "當前變量值:" + reference + "當前版本戳:" + asr.getStamp() + "-"
                    + asr.compareAndSet(reference, reference + "C", oldStamp, oldStamp + 1));

        });

        rightStampThread.start();
        rightStampThread.join();
        errorStampThread.start();
        errorStampThread.join();
        System.out.println(asr.getReference() + "===========" + asr.getStamp());

    }
}

上述代碼中rightStampThread先執行
asr.compareAndSet(oldReferenc, oldReferenc + “Java”, oldStamp, oldStamp + 1)
四個參數的含義依次是,舊值,新值,舊的版本戳,新的版本戳

rightStampThread後執行
asr.compareAndSet(reference, reference + “C”, oldStamp, oldStamp + 1))
它還是使用定義在main方法中的舊版本戳oldStamp,由於這個版本戳已經被rightStampThread改變,所以造成rightStampThread中compareAndSet返回false
在這裏插入圖片描述
本章主要介紹了JAVA中原子操作CAS。讀者要牢記CAS是由硬件指令來實現的,跟鎖🔒不一樣,相信就能辨析它在JAVA併發編程中的作用。

下一章:JAVA併發編程-4-顯式鎖Lock

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章