Java并發(fā)編程 CAS 詳解

一. 書面概述

CAS的全稱為Compare And Swap,直譯就是比較交換。是一條CPU的原子指令,其作用是讓CPU先進行比較兩個值是否相等,然后原子地更新某個位置的值,其實現(xiàn)方式是基于硬件平臺的匯編指令,在intel的CPU中,使用的是cmpxchg指令,就是說CAS是靠硬件實現(xiàn)的,從而在硬件層面提升效率。

CAS有三個操作數(shù):內(nèi)存值V、舊的預(yù)期值A(chǔ)、要修改的值B,當且僅當預(yù)期值A(chǔ)和內(nèi)存值V相同時,將內(nèi)存值修改為B并返回true,否則什么都不做并返回false。

二. sun.misc.Unsafe介紹

工欲善其事必先利其器,為什么要先講Unsafe?

Unsafe類是進行底層操作的方法集合,可以直接操作內(nèi)存,進行一些非常規(guī)操作,所以說是"不安全"的操作,但是因為直接操作內(nèi)存,它的效率很高,通常在在對性能有要求或者有底層操作需求的時候使用。

我們的CAS操作就是通過sun.misc.Unsafe類操作的(java8以下),Unsafe在jdk1.8.0/jre/lib/rt.jar包下。

怎么獲取Unsafe實例?

public final class Unsafe {
    private static final Unsafe theUnsafe;
    private Unsafe() {}
    static{
         theUnsafe = new Unsafe();
    }
    public static Unsafe getUnsafe() {
         return theUnsafe;
    }
}

這里我們沒法直接new對象,必須要通過反射來獲取 theUnsafe 變量,下面來看下里面的幾個重要方法

  1. public long objectFieldOffset(Field f)
    獲取字段的內(nèi)存偏移地址,cas要用。內(nèi)部是native代碼實現(xiàn)的,不講, 看一段實例代碼:
 private static Object unsafe;
    static {
        try {
            /** Unsafe在rt.jar下,不能直接實例化。必須通過反射 */
            Field field = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    static class Data {
        int intParam;
    }
    public static void main(String[] args) throws Exception {
        // 反射獲取objectFieldOffset方法
        Method method = Class.forName("sun.misc.Unsafe").getDeclaredMethod("objectFieldOffset", new Class<?>[] {Field.class});
        method.setAccessible(true);
        // 執(zhí)行調(diào)用, 返回 Data類的intParam成員的偏移地址
        Object ret = method.invoke(unsafe, Data.class.getDeclaredField("intParam"));
        System.err.println(ret);
}

打印結(jié)果 : 12

static靜態(tài)塊就是取得了Unsafe 類中的單例theUnsafe ,然后反射調(diào)用其objectFieldOffset方法,返回對象成員的內(nèi)存偏移量。

  1. public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
    這是重頭戲,CAS操作的方法實現(xiàn), 將對象o的偏移地址變量改成x,前提是x的值是expected,請接著上面的代碼:
    public static void main(String[] args) throws Exception {
        // 反射獲取objectFieldOffset方法
        Method method = Class.forName("sun.misc.Unsafe").getDeclaredMethod("objectFieldOffset", new Class<?>[] {Field.class});
        method.setAccessible(true);
        // 執(zhí)行調(diào)用, 返回 Data類的intParam成員的偏移地址
        long offset = (long) method.invoke(unsafe, Data.class.getDeclaredField("intParam"));
        // 獲取 compareAndSwapInt 方法
        method = Class.forName("sun.misc.Unsafe").getDeclaredMethod("compareAndSwapInt", new Class<?>[] {Object.class,
            long.class,int.class,int.class});
        method.setAccessible(true);
        Data data = new Data();
        data.intParam = 78;
        // 第4個參數(shù): 預(yù)期的值   第5個參數(shù): 要修改的值
        boolean success = (boolean) method.invoke(unsafe, data,offset,7,90);
        System.err.println("1 修改成功嗎:"+success+ " , 修改后intParam:"+data.intParam);
        success = (boolean) method.invoke(unsafe, data,offset,78,90);
        System.err.println("2 修改成功嗎:"+success+ " , 修改后intParam:"+data.intParam);
    }

我們發(fā)現(xiàn)第一次 預(yù)期值傳了7 (實際上是78),所以我們修改失敗,第二次才成功。

  1. public native int getIntVolatile(Object o, long offset);
    獲得給定對象的指定偏移量offset的int值,使用volatile語義,總能獲取到最新的int值。就是獲取的主內(nèi)存的值,并不是自己線程的副本。
    我們都知道JMM內(nèi)存模型 ,線程自己內(nèi)存擁有一套副本,和主內(nèi)存不一致 ,所以一個線程操作一個變量,另一個線程自己的副本不一定馬上會更新,這樣就會導致線程安全。

請看用CAS是如何解決上述問題的

    public static void main(String[] args) throws Exception {
        // 反射獲取objectFieldOffset方法
        Method method = Class.forName("sun.misc.Unsafe").getDeclaredMethod("objectFieldOffset", new Class<?>[] {Field.class});
        method.setAccessible(true);
        // 執(zhí)行調(diào)用, 返回 Data類的intParam成員的偏移地址
        long offset = (long) method.invoke(unsafe, Data.class.getDeclaredField("intParam"));
        // 獲取 compareAndSwapInt 方法
        method = Class.forName("sun.misc.Unsafe").getDeclaredMethod("compareAndSwapInt", new Class<?>[] {Object.class,
            long.class,int.class,int.class});
        method.setAccessible(true);
        Data data = new Data();
        data.intParam = 78;
        
        while(true) {
            method = Class.forName("sun.misc.Unsafe").getDeclaredMethod("getIntVolatile", new Class<?>[] {Object.class,
                long.class});
            //通過 getIntVolatile 方法獲取主內(nèi)存的值
            int expected = (int) method.invoke(unsafe, data,offset);
            // 比較主內(nèi)存的值 和當前 線程副本的值是否一致,一致就更新,否則更新失敗, 
            method = Class.forName("sun.misc.Unsafe").getDeclaredMethod("compareAndSwapInt", new Class<?>[] {Object.class,
                long.class,int.class,int.class});
            boolean success = (boolean) method.invoke(unsafe, data,offset,expected,90);
            System.err.println(success);
            if(success) {
                break;
            }
            // 更新失敗,循環(huán)重試,直到更新成功為止
        }
    }

借助了 getIntVolatile 先獲取主內(nèi)存的值, 然后compareAndSwapInt 將值一直循環(huán)更新成功為止。這其實也就是我們所說的自旋鎖

其實java并發(fā)編程里面的juc包下的,什么AQS啊,AtomicInteger 等都是以上面這種騷操 作基礎(chǔ)的,下面我們看下AtomicInteger 如何騷 的。

三. AtomicInteger 源碼分析

public class AtomicInteger extends Number implements java.io.Serializable {
  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;
}

jdk當然可以直接使用getUnsafe方法來獲取實例,然后把value的內(nèi)存偏移量存儲到valueOffset變量上,后面CAS操作直接用。value 就是AtomicInteger 實際存儲的值。且是 volatile 的

incrementAndGet方法

   public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

 public final int getAndAddInt(Object var1, long var2, int var4) {
   int var5;
   do {
   // 獲取主內(nèi)存的值
      var5 = this.getIntVolatile(var1, var2);
    // 將值變成原有的值var5 加上var4
   } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
   return var5;
}

直接調(diào)用Unsafe的getAndAddInt方法。getAndAddInt在多線程下也是安全的。

get方法

  public final int get() {
        return value;
    }

總結(jié)一下

(1)AtomicInteger中維護了一個使用volatile修飾的變量value,保證可見性;
(2)AtomicInteger中的主要方法最終幾乎都會調(diào)用到Unsafe的compareAndSwapInt()方法保證對變量修改的原子性。

三. CAS總結(jié)

  • CAS機制只能保證共享變量操作的原子性,而不能保證代碼塊的原子性。

  • CAS操作就是基于處理器的CMPXCHG匯編指令實現(xiàn)的,因此,JVM中的CAS的原子性是處理器保障的。CAS是一種樂觀鎖的思想。

  • CAS自旋鎖意思: 發(fā)現(xiàn)線程自己內(nèi)存副本和主內(nèi)存不一致(代表有多線程在競爭操作)就返回修改失敗,然后循環(huán)CAS直到修改成功。

  • CAS解決的問題是: 不加鎖確保某一變量的操作沒有被其他線程修改過。

四. CAS帶來的問題

1. ABA問題

假如你很牛逼,扣款的代碼直接不加鎖而是使用CAS來寫。有這樣一個場景:

  • A賬戶上有10塊錢,娶媳婦需要提款5元,但是系統(tǒng)問題同時發(fā)起了兩次扣款,相當于2個線程1,2并發(fā)。
  • 假如線程1先執(zhí)行CAS,預(yù)期值是10,要修改成5 ,成功。然后準備到線程2,正常情況是 線程2 發(fā)現(xiàn)預(yù)期值是10,現(xiàn)在是5了,就會CAS失敗不扣錢,這樣系統(tǒng)就不會扣兩次錢沒問題, 但是發(fā)生了下面情況。
  • 在線程2 CAS之前,A的媽媽怕兒子娶媳婦錢不夠,又往A賬戶上打了5塊錢,這時,A的賬戶就恢復(fù)了10塊錢。
  • 然后線程2 CAS 發(fā)現(xiàn) 臥槽,預(yù)期值是10,現(xiàn)在也是10,就毫不猶豫把錢扣了。A又只剩5塊了。

媽媽,五塊錢沒了,我不取媳婦了,嗚嗚~~~~~~
其實上述問題原因就是CAS操作將值由A改為B然后又改成A , 另一個線程CAS的話是當做什么都沒發(fā)生的。

看下JDK怎么利用 AtomicStampedReference 來解決這個問題的

public class AtomicStampedReference<V> {
    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }
    private volatile Pair<V> pair;
    public boolean compareAndSet(V   expectedReference,
            V   newReference,
            int expectedStamp,
            int newStamp) {
        // 獲取當前的(元素值,版本號)對
        Pair<V> current = pair;
        return
        // 引用沒變
        expectedReference == current.reference &&
        // 版本號沒變
        expectedStamp == current.stamp &&
        // 新引用等于舊引用
        ((newReference == current.reference &&
        // 新版本號等于舊版本號
        newStamp == current.stamp) ||
        // 構(gòu)造新的Pair對象并CAS更新
        casPair(current, Pair.of(newReference, newStamp)));
        }
        
        private boolean casPair(Pair<V> cmp, Pair<V> val) {
        // 調(diào)用Unsafe的compareAndSwapObject()方法CAS更新pair的引用為新引用
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }
}
  • 首先把我們上面CAS操作的int,變成CAS操作對象Pair,原理是一樣。
  • 加了個版本號stamp,只有版本號不一樣時,CAS才操作成功。
  • 上面代碼流程: 如果元素值和版本號都沒有變化,并且和新的也相同,返回true;如果元素值和版本號都沒有變化,并且和新的不完全相同,就構(gòu)造一個新的Pair對象并執(zhí)行CAS更新pair。

2. 并發(fā)自旋耗cpu多
在并發(fā)量比較低的情況下,線程沖突的概率比較小,自旋的次數(shù)不會很多。但是,高并發(fā)情況下,N個線程同時進行自旋操作,會出現(xiàn)大量失敗并不斷自旋的場景。 JDK8中出現(xiàn)了 LongAdder 來解決AtomicLong的上述并發(fā)大的問題。

AtomicLong中有個內(nèi)部變量value保存著實際的long的值,高并發(fā)場景下,value變量就是N個線程競爭的一個熱點。

LongAdder的基本思路就是分散熱點,將value值分散到一個數(shù)組中,不同線程會命中到數(shù)組的不同槽中,各個線程只對自己槽中的那個值進行CAS操作,這樣熱點就被分散了,沖突的概率就小很多。如果要獲取真正的long值,只要將各個槽中的變量值累加返回即可。

CAS就講到這里吧~ 寫東西太累了,還特別花時間。這些都是上班時間寫的。

《 合抱之木,生于毫末;九層之臺,起于累土;千里之行,始于足下 》
釋義:合抱的大樹,生長于細小的幼苗;九層的高臺,筑起于每一堆泥土;千里的遠行,是從腳下第一步開始走出來的

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容