Java併發編程:什麼是CAS?這回總算知道了

無鎖的思想

衆所周知,Java中對併發控制的最多見方法就是鎖,鎖能保證同一時刻只能有一個線程訪問臨界區的資源,從而實現線程安全。然而,鎖雖然有效,但採用的是一種悲觀的策略。它假設每一次對臨界區資源的訪問都會發生衝突,當有一個線程訪問資源,其餘線程就必須等待,因此鎖是會阻塞線程執行的。java

固然,凡事都有兩面,有悲觀就會有樂觀。而無鎖就是一種樂觀的策略,它假設線程對資源的訪問是沒有衝突的,同時全部的線程執行都不須要等待,能夠持續執行。若是遇到衝突的話,就使用一種叫作CAS (比較交換) 的技術來鑑別線程衝突,若是檢測到衝突發生,就重試當前操做到沒有衝突爲止。算法

CAS概述

CAS的全稱是 Compare-and-Swap,也就是比較並交換,是併發編程中一種經常使用的算法。它包含了三個參數:V,A,B。編程

其中,V表示要讀寫的內存位置,A表示舊的預期值,B表示新值安全

CAS指令執行時,當且僅當V的值等於預期值A時,纔會將V的值設爲B,若是V和A不一樣,說明多是其餘線程作了更新,那麼當前線程就什麼都不作,最後,CAS返回的是V的真實值。bash

而在多線程的狀況下,當多個線程同時使用CAS操做一個變量時,只有一個會成功並更新值,其他線程均會失敗,但失敗的線程不會被掛起,而是不斷的再次循環重試。正是基於這樣的原理,CAS即時沒有使用鎖,也能發現其餘線程對當前線程的干擾,從而進行及時的處理。多線程

CAS的應用類

Java中提供了一系列應用CAS操做的類,這些類位於java.util.concurrent.atomic包下,其中最經常使用的就是AtomicInteger,該類能夠看作是實現了CAS操做的Integer,因此,下面咱們就經過學習該類的案例來一窺全貌CAS的妙用。併發

學習AtomicInteger以前,咱們先來看一段代碼實例:編輯器

public class AtomicDemo {

    public static int NUMBER = 0;

    public static void increase() {
        NUMBER++;
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicDemo test = new AtomicDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++)
                    test.increase();
            }).start();
        }
        Thread.sleep(200);
        System.out.println(test.NUMBER);
    }
}
複製代碼

在main函數中開啓了10個線程,執行後會輪流調用 increase(),固然咱們知道,運行後輸出的結果確定不是咱們指望的值,由於沒有作線程安全的處理,因此10個線程流量操做臨界區的資源NUMBER就會出錯。ide

解決辦法並不難,用咱們以前學過的鎖,例如synchronized修飾代碼塊,程序就會正常輸出10000。固然,用鎖解決並非咱們想要的方式,由於鎖會阻塞線程,影響程序的性能,這時候,AtomicInteger就能夠派上用場了。函數

將上面的程序改造一下,變成下面這樣:

public static AtomicInteger NUMBER = new AtomicInteger(0);

public static void increase() {
    NUMBER.getAndIncrement();
}

public static void main(String[] args) throws InterruptedException {
    AtomicDemo test = new AtomicDemo();
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            for (int j = 0; j < 1000; j++)
                test.increase();
        }).start();
    }
    Thread.sleep(200);
    System.out.println(test.NUMBER);
}
複製代碼

運行main方法,程序輸出的就是咱們想要的值,也就是10000。

上面的代碼中,increase方法裏調用了NUMBER.getAndIncrement() ,這是AtomicInteger的自增方法,會對當前的值加1,而且返回舊值,點進方法的源碼,它調用的是unsafe.getAndAddInt()方法:

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

getAndAddInt的做用是對當前值加1,並返回舊值。

unsafe是Unsafe類的一個變量,經過Unsafe.getUnsafe()來獲取

private static final Unsafe unsafe = Unsafe.getUnsafe();
複製代碼

Unsafe類是一個比較特殊的類,它是一個JDK內部使用的專屬類,用通常的編輯器沒法直接查看源碼,只能看到反編譯後的class文件。

這裏要擴展一個知識點,就是Java自己沒法訪問操做系統,須要使用native方法,而Unsafe類中的方法就包含了大量的native方法,提升了Java對系統底層的原子操做能力。例如咱們代碼中使用到的getAndAddInt()底層就是調用一個native方法,用idea點擊方法,獲得下面反編譯後的代碼:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
複製代碼

compareAndSwapInt的做用是比較並交換整數值,若是指定的字段的值等於指望值,也就是CAS中的 'A' (預期值),那麼就會把它設置爲新值 (CAS中的 'B'),不難想象,該方法內部的實現必然是依靠原子操做完成的。除此以外,Unsafe類中還提供了其餘的原子操做的方法,例如上面源碼中的getIntVolatile就是使用volatile語義得到給定對象的值,這些方法經過底層的原子操做高效的提高了應用層面的性能。

CAS的缺點

雖然CAS的性能比起鎖要強大不少,但它也存在一些缺點,例如:

一、循環的時間開銷大

在getAndAddInt的方法中,咱們能夠看到,只是簡單的設置一個值卻調用了循環,若是CAS失敗,會一直進行嘗試。若是CAS長時間不成功,那麼循環就會不停的跑,無疑會給系統形成很大的開銷。

二、ABA問題

前面說過,CAS判斷變量操做成功的條件是V的值和A是一致的,這個邏輯有個小小的缺陷,就是若是V的值一開始爲A,在準備修改成新值前的期間曾經被改爲了B,後來又被改回爲A,通過兩次的線程修改對象的值仍是舊值,那麼CAS操做就會誤任務該變量歷來沒被修改過。這就是CAS中的「ABA」問題。

固然,"ABA"問題也有解決方案,Java併發包中提供了一個帶有時間戳的對象引用 AtomicStampedReference,其內部不只維護了一個對象值,還維護了一個時間戳,當AtomicStampedReference對應的數值被修改時,除了更新數據自己,還須要更新時間戳,只有對象值和時間戳都知足指望值,才能修改爲功。這是AtomicStampedReference的幾個有關時間戳信息的方法:

//比較設置 參數依次爲:指望值 寫入新值 指望時間戳 新時間戳
public boolean compareAndSet(V expectedReference, V newReference,
                             int expectedStamp, int newStamp)
//得到當前時間戳
public int getStamp()
//設置當前對象引用和時間戳
public void set(V newReference, int newStamp)
複製代碼
相關文章
相關標籤/搜索