並發之CAS無鎖技術


    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來解決

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM