計算機程序的思惟邏輯 (70) - 原子變量和CAS

本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html

從本節開始,咱們探討Java併發工具包java.util.concurrent中的內容,本節先介紹最基本的原子變量及其背後的原理和思惟。java

原子變量

什麼是原子變量?爲何須要它們呢?git

理解synchronized一節,咱們介紹過一個Counter類,使用synchronized關鍵字保證原子更新操做,代碼以下:github

public class Counter {
    private int count;

    public synchronized void incr(){
        count ++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}
複製代碼

對於count++這種操做來講,使用synchronzied成本過高了,須要先獲取鎖,最後還要釋放鎖,獲取不到鎖的狀況下還要等待,還會有線程的上下文切換,這些都須要成本。算法

對於這種狀況,徹底可使用原子變量代替,Java併發包中的基本原子變量類型有:編程

  • AtomicBoolean:原子Boolean類型
  • AtomicInteger:原子Integer類型
  • AtomicLong:原子Long類型
  • AtomicReference:原子引用類型

這是咱們主要介紹的類,除了這四個類,還有一些其餘的類,咱們也會進行簡要介紹。swift

針對Integer, Long和Reference類型,還有對應的數組類型:數組

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

爲了便於以原子方式更新對象中的字段,還有以下的類:安全

  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater
  • AtomicReferenceFieldUpdater

AtomicReference還有兩個相似的類,在某些狀況下更爲易用:bash

  • AtomicMarkableReference
  • AtomicStampedReference

你可能會發現,怎麼沒有針對char, short, float, double類型的原子變量呢?大概是用的比較少吧,若是須要,能夠轉換爲int/long,而後使用AtomicInteger或AtomicLong。好比,對於float,可使用以下方法和int相互轉換:

public static int floatToIntBits(float value) public static float intBitsToFloat(int bits);
複製代碼

下面,咱們先來看幾個基本原子類型,從AtomicInteger開始。

AtomicInteger

基本用法

AtomicInteger有兩個構造方法:

public AtomicInteger(int initialValue) public AtomicInteger() 複製代碼

第一個構造方法給定了一個初始值,第二個的初始值爲0。

能夠直接獲取或設置AtomicInteger中的值,方法是:

public final int get() public final void set(int newValue) 複製代碼

之因此稱爲原子變量,是由於其包含一些以原子方式實現組合操做的方法,好比:

//以原子方式獲取舊值並設置新值
public final int getAndSet(int newValue) //以原子方式獲取舊值並給當前值加1 public final int getAndIncrement() //以原子方式獲取舊值並給當前值減1 public final int getAndDecrement() //以原子方式獲取舊值並給當前值加delta public final int getAndAdd(int delta) //以原子方式給當前值加1並獲取新值 public final int incrementAndGet() //以原子方式給當前值減1並獲取新值 public final int decrementAndGet() //以原子方式給當前值加delta並獲取新值 public final int addAndGet(int delta) 複製代碼

這些方法的實現都依賴另外一個public方法:

public final boolean compareAndSet(int expect, int update) 複製代碼

這是一個很是重要的方法,比較並設置,咱們之後將簡稱爲CAS。該方法以原子方式實現了以下功能:若是當前值等於expect,則更新爲update,不然不更新,若是更新成功,返回true,不然返回false。

AtomicInteger能夠在程序中用做一個計數器,多個線程併發更新,也總能實現正確性,咱們看個例子:

public class AtomicIntegerDemo {
    private static AtomicInteger counter = new AtomicInteger(0);

    static class Visitor extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                counter.incrementAndGet();
                Thread.yield();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int num = 100;
        Thread[] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
            threads[i] = new Visitor();
            threads[i].start();
        }
        for (int i = 0; i < num; i++) {
            threads[i].join();
        }
        System.out.println(counter.get());
    }
}
複製代碼

程序的輸出老是正確的,爲10000。

基本原理和思惟

AtomicInteger的使用方法是簡單直接的,它是怎麼實現的呢?它的主要內部成員是:

private volatile int value;
複製代碼

注意,它的聲明帶有volatile,這是必需的,以保證內存可見性。

它的大部分更新方法實現都相似,咱們看一個方法incrementAndGet,其代碼爲:

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}
複製代碼

代碼主體是個死循環,先獲取當前值current,計算指望的值next,而後調用CAS方法進行更新,若是當前值沒有變,則更新並返回新值,不然繼續循環直到更新成功爲止。

與synchronized鎖相比,這種原子更新方式表明一種不一樣的思惟方式。

synchronized是悲觀的,它假定更新極可能衝突,因此先獲取鎖,獲得鎖後才更新。原子變量的更新邏輯是樂觀的,它假定衝突比較少,但使用CAS更新,也就是進行衝突檢測,若是確實衝突了,那也不要緊,繼續嘗試就行了。

synchronized表明一種阻塞式算法,得不到鎖的時候,進入鎖等待隊列,等待其餘線程喚醒,有上下文切換開銷。原子變量的更新邏輯是非阻塞式的,更新衝突的時候,它就重試,不會阻塞,不會有上下文切換開銷。

對於大部分比較簡單的操做,不管是在低併發仍是高併發狀況下,這種樂觀非阻塞方式的性能都要遠高於悲觀阻塞式方式。

原子變量是比較簡單的,但對於複雜一些的數據結構和算法,非阻塞方式每每難於實現和理解,幸運的是,Java併發包中已經提供了一些非阻塞容器,咱們只須要會使用就能夠了,好比:

  • ConcurrentLinkedQueue和ConcurrentLinkedDeque:非阻塞併發隊列
  • ConcurrentSkipListMap和ConcurrentSkipListSet:非阻塞併發Map和Set

這些容器咱們在後續章節介紹。

但compareAndSet是怎麼實現的呢?咱們看代碼:

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
複製代碼

它調用了unsafe的compareAndSwapInt方法,unsafe是什麼呢?它的類型爲sun.misc.Unsafe,定義爲:

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

它是Sun的私有實現,從名字看,表示的也是"不安全",通常應用程序不該該直接使用。原理上,通常的計算機系統都在硬件層次上直接支持CAS指令,而Java的實現都會利用這些特殊指令。從程序的角度看,咱們能夠將compareAndSet視爲計算機的基本操做,直接接納就好。

實現鎖

基於CAS,除了能夠實現樂觀非阻塞算法,它也能夠用來實現悲觀阻塞式算法,好比鎖,實際上,Java併發包中的全部阻塞式工具、容器、算法也都是基於CAS的 (不過,也須要一些別的支持)。

怎麼實現呢?咱們演示一個簡單的例子,用AtomicInteger實現一個鎖MyLock,代碼以下:

public class MyLock {
    private AtomicInteger status = new AtomicInteger(0);

    public void lock() {
        while (!status.compareAndSet(0, 1)) {
            Thread.yield();
        }
    }

    public void unlock() {
        status.compareAndSet(1, 0);
    }
}
複製代碼

在MyLock中,使用status表示鎖的狀態,0表示未鎖定,1表示鎖定,lock()/unlock()使用CAS方法更新,lock()只有在更新成功後才退出,實現了阻塞的效果,不過通常而言,這種阻塞方式過於消耗CPU,有更爲高效的方式,咱們後續章節介紹。MyLock只是用於演示基本概念,實際開發中應該使用Java併發包中的類如ReentrantLock。

AtomicBoolean/AtomicLong/AtomicReference

AtomicBoolean/AtomicLong/AtomicReference的用法和原理與AtomicInteger是相似的,咱們簡要介紹下。

AtomicBoolean

AtomicBoolean能夠用來在程序中表示一個標誌位,它的原子操做方法有:

public final boolean compareAndSet(boolean expect, boolean update) public final boolean getAndSet(boolean newValue) 複製代碼

實際上,AtomicBoolean內部使用的也是int類型的值,用1表示true, 0表示false,好比,其CAS方法代碼爲:

public final boolean compareAndSet(boolean expect, boolean update) {
    int e = expect ? 1 : 0;
    int u = update ? 1 : 0;
    return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
複製代碼

AtomicLong

AtomicLong能夠用來在程序中生成惟一序列號,它的方法與AtomicInteger相似,就不贅述了。它的CAS方法調用的是unsafe的另外一個方法,如:

public final boolean compareAndSet(long expect, long update) {
    return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
複製代碼

AtomicReference

AtomicReference用來以原子方式更新複雜類型,它有一個類型參數,使用時須要指定引用的類型。如下代碼演示了其基本用法:

public class AtomicReferenceDemo {
    static class Pair {
        final private int first;
        final private int second;

        public Pair(int first, int second) {
            this.first = first;
            this.second = second;
        }

        public int getFirst() {
            return first;
        }

        public int getSecond() {
            return second;
        }
    }

    public static void main(String[] args) {
        Pair p = new Pair(100, 200);
        AtomicReference<Pair> pairRef = new AtomicReference<>(p);
        pairRef.compareAndSet(p, new Pair(200, 200));

        System.out.println(pairRef.get().getFirst());
    }
}
複製代碼

AtomicReference的CAS方法調用的是unsafe的另外一個方法:

public final boolean compareAndSet(V expect, V update) {
    return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
複製代碼

原子數組

原子數組方便以原子的方式更新數組中的每一個元素,咱們以AtomicIntegerArray爲例來簡要介紹下。

它有兩個構造方法:

public AtomicIntegerArray(int length) public AtomicIntegerArray(int[] array) 複製代碼

第一個會建立一個長度爲length的空數組。第二個接受一個已有的數組,但不會直接操做該數組,而是會建立一個新數組,只是拷貝參數數組中的內容到新數組。

AtomicIntegerArray中的原子更新方法大多帶有數組索引參數,好比:

public final boolean compareAndSet(int i, int expect, int update) public final int getAndIncrement(int i) public final int getAndAdd(int i, int delta) 複製代碼

第一個參數i就是索引。看個簡單的例子:

public class AtomicArrayDemo {
    public static void main(String[] args) {
        int[] arr = { 1, 2, 3, 4 };
        AtomicIntegerArray atomicArr = new AtomicIntegerArray(arr);
        atomicArr.compareAndSet(1, 2, 100);
        System.out.println(atomicArr.get(1));
        System.out.println(arr[1]);
    }
}
複製代碼

輸出爲:

100
2
複製代碼

FieldUpdater

FieldUpdater方便以原子方式更新對象中的字段,字段不須要聲明爲原子變量,FieldUpdater是基於反射機制實現的,咱們會在後續章節介紹反射,這裏簡單介紹下其用法,看代碼:

public class FieldUpdaterDemo {
    static class DemoObject {
        private volatile int num;
        private volatile Object ref;

        private static final AtomicIntegerFieldUpdater<DemoObject> numUpdater
            = AtomicIntegerFieldUpdater.newUpdater(DemoObject.class, "num");
        private static final AtomicReferenceFieldUpdater<DemoObject, Object>
            refUpdater = AtomicReferenceFieldUpdater.newUpdater(
                    DemoObject.class, Object.class, "ref");

        public boolean compareAndSetNum(int expect, int update) {
            return numUpdater.compareAndSet(this, expect, update);
        }

        public int getNum() {
            return num;
        }

        public Object compareAndSetRef(Object expect, Object update) {
            return refUpdater.compareAndSet(this, expect, update);
        }

        public Object getRef() {
            return ref;
        }
    }

    public static void main(String[] args) {
        DemoObject obj = new DemoObject();
        obj.compareAndSetNum(0, 100);
        obj.compareAndSetRef(null, new String("hello"));
        System.out.println(obj.getNum());
        System.out.println(obj.getRef());
    }
}
複製代碼

類DemoObject中有兩個成員num和ref,聲明爲volatile,但不是原子變量,不過DemoObject對外提供了原子更新方法compareAndSet,它是使用字段對應的FieldUpdater實現的,FieldUpdater是一個靜態成員,經過newUpdater工廠方法獲得,newUpdater須要的參數有類型、字段名、對於引用類型,還須要引用的具體類型。

ABA問題

使用CAS方式更新有一個ABA問題,該問題是指,一個線程開始看到的值是A,隨後使用CAS進行更新,它的實際指望是沒有其餘線程修改過才更新,但普通的CAS作不到,由於可能在這個過程當中,已經有其餘線程修改過了,好比先改成了B,而後又改回爲了A。

ABA是否是一個問題與程序的邏輯有關,若是是一個問題,一個解決方法是使用AtomicStampedReference,在修改值的同時附加一個時間戳,只有值和時間戳都相同才進行修改,其CAS方法聲明爲:

public boolean compareAndSet( V expectedReference, V newReference, int expectedStamp, int newStamp) 複製代碼

好比:

Pair pair = new Pair(100, 200);
int stamp = 1;
AtomicStampedReference<Pair> pairRef = new AtomicStampedReference<Pair>(pair, stamp);
int newStamp = 2;
pairRef.compareAndSet(pair, new Pair(200, 200), stamp, newStamp);
複製代碼

AtomicStampedReference在compareAndSet中要同時修改兩個值,一個是引用,另外一個是時間戳,它怎麼實現原子性呢?實際上,內部AtomicStampedReference會將兩個值組合爲一個對象,修改的是一個值,咱們看代碼:

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) ||
         casPair(current, Pair.of(newReference, newStamp)));
}
複製代碼

這個Pair是AtomicStampedReference的一個內部類,成員包括引用和時間戳,具體定義爲:

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);
    }
}
複製代碼

AtomicStampedReference將對引用值和時間戳的組合比較和修改轉換爲了對這個內部類Pair單個值的比較和修改。

AtomicMarkableReference是另外一個AtomicReference的加強類,與AtomicStampedReference相似,它也是給引用關聯了一個字段,只是此次是一個boolean類型的標誌位,只有引用值和標誌位都相同的狀況下才進行修改。

小結

本節介紹了各類原子變量的用法以及背後的原理CAS,對於併發環境中的計數、產生序列號等需求,考慮使用原子變量而非鎖,CAS是Java併發包的基礎,基於它能夠實現高效的、樂觀、非阻塞式數據結構和算法,它也是併發包中鎖、同步工具和各類容器的基礎。

下一節,咱們討論併發包中的顯式鎖。

(與其餘章節同樣,本節全部代碼位於 github.com/swiftma/pro…)


未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。

相關文章
相關標籤/搜索