1. 程式人生 > >並發之CAS無鎖技術

並發之CAS無鎖技術

兩個 javap 執行 update ID get 變量 dset 變化

CAS算法即是:Compare And Swap,比較並且替換;
CAS算法存在著三個參數,內存值V,舊的預期值A,以及要更新的值B。當且僅當內存值V和預期值B相等的時候,才會將內存值修改為B,否則什麽也不做,直接返回false; 比如說某一個線程要修改某個字段的值,當這個值初始化的時候會在內存中完成,根據Java內存模型,該線程保存著這個變量的一個副本;當且僅當這個變量的副本和內存的值如果相同,那麽就可以完成對值得修改,並且這個CAS操作完全是原子性的操作,也就是說此時這個操作不可能被中斷。 先來看一個n++的問題:
public class Case {
    
public volatile int n; public void add() { n++; } }

上述代碼中什麽變量被volatile修飾,此時說明該變量在多線程操作的情況下可以保證內存的可見性,但是不可以保證原子性操作,因此在多線程並發的時候還是會出現問題的;利用Javap命令來看看匯編指令:

PS D:\ssh> javac Case.java
PS D:\ssh> javap -c Case
Compiled from "Case.java"
public class Case {
  public volatile int n;

  public Case();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field n:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field n:I
      10: return
}
PS D:\ssh>

  

在方法add()中,第17行表示獲取到了n的初始值; 第19行執行了iadd()操作,n加一; 第20行執行了putfield,把新累加的值賦值給n; 在上面我很清楚的說過volatile確實無法保證上述三個操作步驟的原子性;可以使用synchrnoized的方法完成原子性的操作;synchrnoized是互斥鎖,也是可重入的鎖,可以保證操作的原子性;但是加鎖之後效率降低, 好了,接下來再看一段代碼:
public int a = 1;
public boolean compareAndSwapInt(int
b) { if (a == 1) { a = b; return true; } return false
; }
上述方法在並發的情況下也是會出現問題的;當多個線程直接進入compareAndSwapInt()之後,他們也同時符合上述的邏輯判斷,此時對a的賦值也有可能同事發生,這樣也帶來了線程安全的問題;
同樣加鎖的方式也可以解決這個問題,但是在這裏我們不研究鎖的問題;下面我們來看看一段代碼,這是AtomicInteger類中的一部分源碼:
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    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;


    /**
     * Gets the current value.
     *
     * @return the current value
     */
    public final int get() {
        return value;
    }
}

1 Unasfe是CAS的核心類,通過這個類可以獲取字段在內存中的地址偏移量;Unsafe是native的,我們一般不可能使用;這是Java對硬件操作的支持; 2 valueOffset是地址偏移量(變量在內存中的地址偏移量) 3 value是使用volatile修飾的,保證了內存的可見性; 平時做常用的方法addAndGet()方法;作用是原子性的操作給變量添加值;
int addAndGet(int delta) 以原子方式將給定值與當前值相加。

在Java8中,這個方法的實現是調用了unsafe()方法;因此我們看不到;

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

但是通過網上看到了該方法的實現方式:

public final int addAndGet(int delta) {
    for (;;) {
        int current = get();
        int next = current + delta;
        if (compareAndSet(current, next))
            return next;
    }
}
  public final int get() {
        return value;
    }
假設delta的值為1,在CAS算法下操作的話,首先進入一個for循環體;假設存在著兩個線程,並且內存中的值value=3;根據Java內存模型,每一個線程都存在這這個變量的副本; 1) 線程1進入循環體,獲取到current的值為3,然後獲取到到next的值此時為4;此時假設線程1運氣不好,被掛起; 2)線程2進入循環體,獲取到current的值為3,同時next的值也為4;線程2運氣好,此時繼續執行compareAndSet()方法;
public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
 }
線程2傳入兩個參數,一個當前值,以及一個預期值;當前值,也就是current=3.要修改成為4;此時當前值也就是預期值和內存中的value比較,此時都是3,那麽修改內存中的值為4; 3)線程1此時再次執行compareAndSwapInt()方法的時候。發現內存中的值為4,預期的值是3,兩者不相等,此時就不可以再次賦值了; CAS的缺點: CAS存在和“ABA的漏洞”;什麽是ABA呢? 假定在某個時刻某個線程從內存中取出A,然後在下個時刻準備更新這個值;在這個時間差內數據發生了改變; 技術分享圖片 假設線程1從內存中取出了A,線程2也從內存中取出了A,並且將值修改為B,最後又改為A,當線程1去更新值得時候發現內存中的數據和線程備份數據相同,可以更新;但是此時內存中的值其實發生了變化的,只不過又變回去了;在實際的開發過程中,ABA可能會帶來一些問題,但是我認為無關緊要,我們需要的只是數值的變化而已; 對於單向鏈表實現的棧而言;假設存在一個鏈表 A---->B;線程1要去將棧頂的數據修改為B,但是此時線程2進來之後,A---->B出棧,D、C、A壓棧;此時鏈表的結構發生了變化;A---->C---->D;此時線程1發現棧頂元素還是A,而元素B被出棧之後成為一個遊離的對象, 解決方式:由於CAS算法沒有直接的使用鎖;而是通過樂觀鎖的方式去控制並發的;而對於樂觀鎖而言一般都是操作+時間戳來控制每一次的版本號的;在JDK類庫中,可以使用AutomicStampReference來解決

並發之CAS無鎖技術