Java多線程:CAS與java.util.concurrent.atomic

鎖的幾種概念

悲觀鎖

老是假設最壞的狀況,每次獲取數據都認爲別人會修改,因此拿數據時會上鎖,一直到釋放鎖不容許其餘線程修改數據。Java中如synchronized和reentrantLock就是這種實現。html

樂觀鎖

老是假設最好的狀況,每次去拿數據時都認爲別人不會修改,因此不上鎖,等更新數據時判斷一下在此期間是否有其餘人更新過這個數據,可使用CAS算法實現。樂觀鎖適用於多讀少寫的應用類型,能夠大幅度提升吞吐量。樂觀鎖的實現機制主要包括版本號機制(給數據加一個版本號,數據被修改版本號會加一,更新時讀取版本號,若讀取到的版本號和以前一致才更新,不然駁回)和CAS算法(下詳)。java

自旋鎖與互斥鎖

多線程互斥訪問時會進入鎖機制。互斥設計時會面臨一個狀況:沒有得到鎖的進程如何處理。一般有兩種辦法:一種是沒有得到鎖就阻塞本身,請求OS調度另外一個線程上的處理器,即互斥鎖;另外一種時沒有得到鎖的調用者就一直循環,直到鎖的持有者釋放鎖,即自旋鎖。面試

自旋鎖是一種較低級的保護數據的方式,存在兩個問題:遞歸死鎖,即遞歸調用時試圖得到相同的自旋鎖。過多佔用CPU資源,自旋鎖不成功時會持續嘗試,一般一個自旋鎖會有參數限制嘗試次數,超出後放棄time slice,等待一下一輪機會。算法

但在鎖持有者保持鎖的時間較短的前提下,選擇自旋而非睡眠則大大提升了效率,於是在這種狀況下自旋鎖效率遠高於互斥鎖。編程

CAS

CAS算法

CAS即compare and swap,是一種系統原語,是不可分割的操做系統指令。CAS是一種樂觀鎖實現。數組

CAS有三個操做數,內存值V,舊的預期內存值A,要修改的新值B,當且僅當A=V,纔將內存值V修改成B,不然不會執行任何操做。通常狀況下CAS是一個自旋操做,即不斷重試。緩存

CAS開銷

CAS是CPU指令集的操做,只有一步的原子操做,因此很是快,CAS的開銷主要在於cache miss問題。如圖多線程

這是一個8核CPU系統,共有4個管芯,每一個管芯中有兩個CPU,每一個CPU有cache,管芯內有一個互聯模塊,讓管芯的兩個核能夠互相通訊。圖中的系統鏈接模塊可讓四個管芯通訊。例如,此時CPU0進行一個CAS操做,而該變量所在的緩存線在CPU7的高速緩存中,則流程以下:併發

  • CPU檢查本地緩存,沒有找到緩存線。
  • 請求被轉發到CPU0和CPU1的互聯模塊,檢查CPU1的本地高速緩存,沒有找到緩存線。
  • 請求被轉發到系統互聯模塊,檢查其餘三個管芯,得知緩存線在CPU6和CPU7所在的管芯中。
  • 請求被轉發到CPU6和CPU7的互聯模塊,檢查這兩個CPU的高速緩存,在CPU7中找到緩存線。
  • CPU7將緩存線發給互聯模塊,並刷新本身的緩存線。
  • CPU6和CPU7的互聯模塊將緩存線發送給系統互聯模塊。
  • 系統互聯模塊將緩存線發送給CPU0和CPU1的互聯模塊。
  • CPU0對高速緩存中的變量執行CAS操做。

Java中的CAS

JDK5增長java.util.concurrent包,其中不少類使用了CAS操做。這些CAS操做基於Unsafe類中的native方法實現:app

//第一個參數o爲給定對象,offset爲對象內存的偏移量,經過這個偏移量迅速定位字段並設置或獲取該字段的值,
//expected表示指望值,x表示要設置的值,下面3個方法都經過CAS原子指令執行操做,
//設置成功返回true,不然返回false。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

因爲CAS做用的對象在主存裏而不是在線程的高速緩存裏,CAS操做在Java中須要配合volatile使用。

Java中的CAS主要包含如下幾個問題:

  • ABA問題,即變量V初次讀時是A值,被賦值時也是A值,但期間變量被賦值成B值,CAS會誤認爲他從沒被修改過。AtomicStampedReference和AtomicMarckableReference類提供了監測ABA問題的能力,其中的compareAndSet方法首先檢查當前引用是否等於預期引用,而且當前標誌等於預期標誌,所有相等則以原子方式將該引用和該標誌的值設置爲給定的更新值。
  • 循環開銷,自旋CAS長時間不成功會給CPU帶來很是大的執行開銷。若JVM能支持pause命令,效率有必定提高。由於pause命令一方面能夠延遲流水線執行命令,使CPU不會消耗過多的執行資源,另外一方面能夠避免退出循環時由內存順序衝突引發的CPU流水線被衝突,從而提升CPU的執行效率。
  • 只能保證一個共享變量的原子操做,當操做涉及跨多個共享變量時CAS無效。可用AtomicReference封裝多個字段來保證引用對象之間的原子性。

CAS與synchronized

  • 資源競爭少時,synchronized同步鎖進行線程阻塞,喚醒切換,用戶內核態間切換,浪費額外CPU資源,CAS基於硬件實現,不進入內核,不切換線程,操做自旋概率小,CAS有更高的性能。
  • 資源競爭嚴重時,CAS自旋機率較大,從而浪費更多的CPU資源,效率低於synchronized。

java.util.concurrent.atomic

jdk1.5提供了一組原子類,由CAS對其實現。其中的類能夠分爲四組:

  • AtomicBoolean,AtomicInteger,AtomicLong 基本類型,bool, int, long
  • AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray 數組類型,包括整形數組,長整型數組,引用類型數組
  • AtomicReference,AtomicStampedReference,AtomicMarkableReference AtomicReference爲普通的引用類型原子類,AtomicStampedReference在構造方法中加入了stamp(相似時間戳)做爲標識,採用自增int做爲stamp,在stamp不重複的前提下能夠解決ABA問題,AtomicStampedReference能夠獲知引用被更改了幾回。當咱們不須要知道引用被更改幾回僅須要知道引用是否被更改過,則可使用AtomicMarkableReference,這個類用boolean變量表示變量是否被更改過。
  • AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater 三種原子更新對應類型(int, long, 引用)的更新器,用於對普通類進行原子更新。

其做用爲對單一數據的操做實現原子化,無需阻塞代碼,但訪問兩個或兩個以上的atomic變量或對單個atomic變量進行2次或2次以上的操做被認爲是須要同步的以便這些操做是一個原子操做。

AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference, AtomicStampedReference, AtomicMarkableReference

前四種類型用來處理Boolean,Integer, Long, 對象,後兩個類支持的方法和AtomicReference基本一致,僅做用不一樣。以上類型均包含如下方法:

  • 構造函數,默認值分別爲false, 0, 0, null。帶參數則參數爲初始化數據。
  • set(newValue)和get()方法,常規的設置/讀取值,非原子操做。其中set是volatile操做。
  • lazySet(newValue),設置值,原子操做,調用後的一小段時間其餘線程可能會讀取到舊值。
  • getAndSet(newValue)至關於先使用get再set,可是是一個原子操做。
  • compareAndSet(expectedData, newData),接受兩個參數,若atomic內數據和指望數據一致,則將新數據賦值給atomic數據返回true,不然不設置並返回false。
  • weakCompareAndSet(expectedData, newData),與前者相似,但更高效,不一樣的是可能會返回虛假的失敗,不提供排序的保證,最好用於無關於happens-before的程序。

對於AtomicInteger, AtomicLong,還實現了getAndIncrement(), increateAndGet(), getAndDecreate(), decreateAndGet(), addAndGet(delta), getAndAdd(delta)方法,以實現加減法的原子操做。

AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray

這三種類型用於處理數組,經常使用方法以下:

  • set(index, newValue)和get(index)方法,常規的設置/讀取索引對應值,非原子操做。其中set是volatile操做。
  • lazySet(index, newValue),設置索引對應值,原子操做,調用後的一小段時間其餘線程可能會讀取到舊值。
  • getAndSet(index, newValue)至關於先使用get再set,可是是一個原子操做。
  • compareAndSet(index, expectedData, newData),接受三個參數,索引,指望數據,新數據。若atomic內數據和指望數據一致,則將新數據賦值給atomic數據返回true,不然不設置並返回false。

對於AtomicIntegerArray, AtomicLongArray,還實現了getAndIncrement(index), increateAndGet(index), getAndDecreate(index), decreateAndGet(index), addAndGet(index, delta), getAndAdd(index, delta)方法,以實現加減法的原子操做。

AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater

這三種類型用於處理普通對象中某個字段的CAS更新,因爲是CAS更新,要求該字段必須是volatile的,經常使用方法以下:

  • AtomicReferenceFiledUpdater.newUpdater(holderClassName, fieldClassName, fieldNameString):對於普通的引用更新器,建立一個更新器須要如下三個參數:指定的類的類型,類中要更新的字段的類型,該字段的名字。該方法使用反射尋找須要更新的字段,且因爲字段是成員變量,須要特別注意要可以訪問到字段。對於AtomicIntegerFieldUpdater和AtomicLongFieldUpdater,因爲已經肯定了字段類型,只須要提供指定的類的類型和字段名便可。
  • lazySet(object, newValue),設置值,原子操做,調用後的一小段時間其餘線程可能會讀取到舊值。
  • getAndSet(object, newValue)至關於先使用get再set,可是是一個原子操做。
  • compareAndSet(object, expectedData, newData),接受兩個參數,若atomic內數據和指望數據一致,則將新數據賦值給atomic數據返回true,不然不設置並返回false。

示例操做以下:

User類(由普通類改形成的CAS更新類)

public class User {
    private static AtomicReferenceFieldUpdater<User, String> nameUpdater = AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");
    private static AtomicIntegerFieldUpdater<User> ageUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
    private volatile String name;
    private volatile int age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public void lazySetName(String name) {
        nameUpdater.lazySet(this, name);
    }

    public String getSetName(String name) {
        return nameUpdater.getAndSet(this, name);
    }

    public void compareAndSetName(String exceptedName, String newName) {
        nameUpdater.compareAndSet(this, exceptedName, newName);
    }

    public void lazySetAge(int age) {
        ageUpdater.lazySet(this, age);
    }

    public Integer getSetAge(int age) {
        return ageUpdater.getAndSet(this, age);
    }

    public void compareAndSetAge(int exceptedAge, int newAge) {
        ageUpdater.compareAndSet(this, exceptedAge, newAge);
    }
}

主程序

public class AtomicTest {
    public void run() {
        User user = new User("Atomic", 10);
        user.compareAndSetName("Atomic", "Ass");
        user.compareAndSetAge(10, 11);
        System.out.println(user.getName() + user.getAge());
    }

    public static void main(String[] args) throws Exception {
        new AtomicTest().run();
    }
}

輸出結果:

Ass11

參考文獻

深刻理解CAS算法原理
面試必備之樂觀鎖與悲觀鎖
Java之多線程 Atomic(原子的)
對 volatile、compareAndSet、weakCompareAndSet 的一些思考
併發編程面試必備:JUC 中的 Atomic 原子類總結
AtomicReference,AtomicStampedReference與AtomicMarkableReference的區別
JAVA中的CAS

相關文章
相關標籤/搜索