解密JUC——非阻塞同步指令CAS

一、爲什麼使用CAS?CAS是啥?

在多線程中,爲了保證一系列的操作具有原子性,獨佔鎖是比較簡單實用的同步機制,但它是一項悲觀技術,對系統性能有嚴重的損耗,因爲它假設了最壞的情況:如果你不鎖門,那麼搗蛋鬼就會闖入並搞得一團糟。

對於細粒度的操作,還有另外一種更加高效的辦法,可以在不發生干擾的情況下完成更新操作。這種方法需要藉助衝突檢查機制來判斷在更新過程中是否存在來自其它線程的干擾,如果存在,這個操作將會失敗,並且可以選擇是否重試。這種方法有個好聽的名字——樂觀鎖

現在很多計算機針對多處理器提供了一些特殊指令,用於管理對共享數據的併發訪問。採用比較多的方法是實現一個比較並交換的指令,即Conmpare And Swap,簡稱CAS。

CAS包含了3個操作數——需要讀寫的內存位置V、進行比較的值A和擬寫入的新值B。當且僅當V的值等於A時,CAS纔會通過原子方式用新值B來更新V的值,否則不會執行任何操作。無論位置V的值是否等於A,都將返回V原有的值。

二、Java中CAS的實現 

Java對CAS的支持體現在Unsafe類上,裏面基本都是本地方法 ,說明它是調用底層操作系統指令實現的。看它的名字:sun.misc.Unsafe,沒有源碼也沒有註釋,它不屬於JDK的標準,也不推薦用戶去搞。

Unsafe類使用了單例模式,但是它的getUnsafe方法有個判斷,調用類必須是由BootstrapClassLoader加載的才行。

    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

如果我們代碼裏面直接調用getUnsafe的話是會報異常的。

三、Unsafe類的用法 

雖然我們不能直接調用getUnsafe方法,但是我們可以通過反射暴力訪問獲取Unsafe實例:可以選擇讀取字段的值或者構造器創建對象。

        // 方式一
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Assert.assertNotNull(field.get(null));
        // 方式二
        Constructor<?> declaredConstructor = Unsafe.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Assert.assertNotNull(declaredConstructor.newInstance());

Unsafe類對應CAS的方法爲compareAndSwapXxx,其中Xxx表示內存中這個值的類型,它們的返回值是表示成功與否的布爾類型。這些方法一共有4個參數,第一個是要修改的對象,第二個是對象指定字段的偏移量,第三個是期望值,第四個是目標值。

下面示範一下使用這個類修改一個對象的指定屬性:定義一個只有name屬性的Person類並創建它的實例,分別傳入對的和錯的期望值參數試圖去修改它的name屬性,結果可以看到,第一次成功了,第二次失敗了。

    public void testModifyObjAttr() throws NoSuchFieldException, IllegalAccessException {
        // 獲取實例
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe)field.get(Unsafe.class);

        // 獲得Person類的name屬性的偏移量
        long fieldOffset = unsafe.objectFieldOffset(Person.class.getDeclaredField("name"));

        // 修改值:傳入正確期望值
        Person person = new Person();
        boolean result = unsafe.compareAndSwapObject(person, fieldOffset, null, "huang");
        Assert.assertEquals("huang", person.name);

        // 修改值:傳入錯誤期望值
        Assert.assertFalse(unsafe.compareAndSwapObject(person, fieldOffset, "zhou", "hua"));
    }

    private static class Person {
        private String name;
    }

四、JUC中用到CAS的地方

Java併發工具包裏面的原子類和AQS都用到了Unsafe。

比如AtomicInteger類裏面類似i++的原子操作

 

比如AQS裏面state值的修改

 

五、CAS的弊端

1、ABA問題

舉個栗子:線程1從內存X中取出A,這時候另一個線程2也從內存X中取出A,然後線程2進行了一些操作將內存X中的值變成了B,緊接着線程2又將內存X中的數據變回了A,這時候線程1進行CAS操作發現內存X中仍然是A,所以線程1操作成功。雖然線程1的CAS操作成功,但是整個過程就是有問題的,因爲這個過程內存X中的值是變化過的。比如鏈表的頭在變化了兩次後恢復了原值,但是不代表鏈表就沒有變化。

解決辦法:數據增加一個標記,比如版本號之類的,在CAS的時候通過額外對這個標識校驗來判斷數據是否有過變更。

Java中提供了AtomicStampedReference類和AtomicMarkableReference類來處理會發生ABA問題的場景。

2、不適合高併發的場景

比如很多線程同時要將一個對象的屬性由A改成B,那麼CAS操作最終只有一個可以成功,但是太多失敗的衝突檢查帶來了不必要的性能損耗,倒不如用獨佔鎖來得簡單粗暴。

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