Java編程拾遺『原子變量』

本篇文章我們來講java.util.concurrent包下的原子變量,原子變量的引入主要是爲了解決普通變量(int、Integer、Long等)修改操作不是原子的,進而導致必須使用同步機制才能保證安全更新的問題。舉個例子:

public class Counter {
    private int count;

    public void incr(){
        count ++;
    }
    
    public int getCount() {
        return count;
    }
}

Counter類有兩個方法,incr方法用於對成員變量count自增加1,getCount方法用於獲取count值,通過如下方式使用:

public class Test {

    public static class Counter {
        private int count;

        public void incr() {
            count++;
        }

        public int getCount() {
            return count;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    counter.incr();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            threads[i].start();
        }

        for (int i = 0; i < 5; i++) {
            threads[i].join();
        }

        System.out.println(counter.getCount());
    }
}

main方法中,開啓五個線程,每個線程的調用counter的incr方法10次,那麼理論上最後counter調用getCount()方法獲取的結果是50。但是通過多次執行發現,結果不一定是50,有時候可能是49,有時候可能是48。原因在之前講synchronized使用的那篇文章講過,因爲++操作不是原子的。一個++操作涉及三個動作:讀取、加1、回寫。那麼就有可能導致多線程執行時,多個線程讀取到同一個值,導致最終count的值小於50。

解決辦法之前也講過,就是把incr和getCount方法聲明爲synchronized方法。但是對於count++這種操作來說,使用synchronzied成本太高了(雖然synchronized在Java6之後進行了一系列優化),獲取鎖、釋放鎖以及相關的上下文切換等,這些都需要成本。

對於這種情況,完全可以使用原子變量代替,Java併發包中的基本原子變量類型有:

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

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

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

原子方式修改對象內部成員變量類型:

  • AtomicReferenceFieldUpdater

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

  • AtomicMarkableReference
  • AtomicStampedReference

可以發現,沒有針對char,short,float,double類型的原子變量。有可能是因爲這幾種類型使用的相對較少吧。如果需要,可以轉換爲int/long,然後使用AtomicInteger或AtomicLong。比如,對於float,可以使用Float類如下方法和int相互轉換:

public static int floatToIntBits(float value)
public static float intBitsToFloat(int bits)

1. AtomicInteger

1.1 基本方法說明

S.N. 方法 說明
1 public AtomicInteger(int initialValue) 構造函數,給定初始值
2 public AtomicInteger() 構造函數,初始值爲0
3 public final int get() 獲取AtomicInteger中的值
4 public final void set(int newValue) 設置AtomicInteger中的值
5 public final boolean compareAndSet(int expect, int update) CAS,expect爲期望的值,update爲修改的值
6 public final int getAndSet(int newValue) 以原子方式設置新值,並返回舊值
7 public final int getAndIncrement() 以原子方式將當前值加1,並返回舊值
8 public final int getAndDecrement() 以原子方式將當前值減1,並返回舊值
9 public final int getAndAdd(int delta) 以原子方式將當前值加delta,並返回舊值
10 public final int incrementAndGet() 以原子方式將當前值加1,並返回新值
11 public final int decrementAndGet() 以原子方式將當前值減1,並返回新值
12 public final int addAndGet(int delta) 以原子方式將當前值加delta,並返回新值
13 public final int getAndUpdate(IntUnaryOperator updateFunction) Java8新方法,以原子方式將當前值更新爲函數結果,並返回舊值
14 public final int updateAndGet(IntUnaryOperator updateFunction) Java8新方法,以原子方式將當前值更新爲函數結果,並返回新值
15 public final int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction) Java8新方法,以原子方式將當前值更新爲函數結果,並返回舊值
16 public final int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) Java8新方法,以原子方式將當前值更新爲函數結果,並返回新值

這裏重點提一下compareAndSet方法,該方法以原子方式實現瞭如下功能:如果當前值等於expect,則更新爲update,否則不更新,如果更新成功,返回true,否則返回false。(依賴於Unsafe類,Unsafe類的方法都試native方法)

AtomicInteger可以在程序中用作一個計數器,多個線程併發更新,也總能實現正確性,比如上面的例子改爲:

public class Test1 {
    public static class Counter {
        private AtomicInteger count;

        public Counter(AtomicInteger count) {
            this.count = count;
        }

        public void incr() {
            count.incrementAndGet();
        }

        public int getCount() {
            return count.get();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter(new AtomicInteger());

        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    counter.incr();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            threads[i].start();
        }

        for (int i = 0; i < 5; i++) {
            threads[i].join();
        }

        System.out.println(counter.getCount());
    }
}

因爲incrementAndGet()方法是原子的,所以程序總能輸出正確的結果50,雖然沒有使用synchronized同步。

1.2 AtomicInteger基本原理

AtomicInteger類聲明爲:

public class AtomicInteger extends Number implements java.io.Serializable {

    // 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;

    //Methods
}

Unsafe類提供Java直接訪問系統內存資源、自主管理內存的入口,提供了硬件級別的原子操作,使Java語言擁有了類似C語言指針一樣操作內存空間的能力。AtomicInteger類中的Unsafe成員變量,就是用來操作成員變量value的內存空間的(實現原子方式改變value值)。

成員變量value就是用來存儲值的,並且聲明爲volatile,所以可以保證可見性。而value訪問的的原子性就是通過上述unsafe成員變量實現的。

1.2.1 incrementAndGet

AtomicInteger類中大部分更新方法實現都很類似,我們先來分析一下incrementAndGet實現:

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

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

    return var5;
}

Unsafe類的getAndAddInt方法,就是一直執行CAS操作,將當前值var5改爲var5 + var4,並返回更改前的舊值var5。

與synchronized鎖相比,這種原子更新方式代表一種不同的思維方式。

  • synchronized是悲觀的,它假定更新很可能衝突,所以先獲取鎖,得到鎖後才更新。原子變量的更新邏輯是樂觀的,它假定衝突比較少,但使用CAS更新,也就是進行衝突檢測,如果確實衝突了,那就繼續嘗試使用CAS更新
  • synchronized代表一種阻塞式算法,得不到鎖的時候,進入鎖等待隊列,等待其他線程喚醒,有上下文切換開銷。原子變量的更新邏輯是非阻塞式的,更新衝突的時候,它就重試,不會阻塞,不會有上下文切換開銷

對於大部分比較簡單的操作,無論是在低併發還是高併發情況下,這種樂觀非阻塞方式的性能都要遠高於悲觀阻塞式方式。

1.2.2 compareAndSet

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

Unsafe類中compareAndSwapInt方法是native方法:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5)

一般的計算機系統都在硬件層次上直接支持CAS指令,compareAndSwapInt肯定也是通過這些指令實現的CAS。關於Unsafe類,美團技術博客有篇文章講述的很清楚,Java魔法類:Unsafe應用解析,感興趣的同學可以去了解一下。

1.2.3 getAndUpdate(IntUnaryOperator updateFunction)

public final int getAndUpdate(IntUnaryOperator updateFunction) {
    int prev, next;
    do {
        prev = get();
        //將prev應用於函數式表達式,獲取新值,並循環通過CAS將當前值改爲新值
        next = updateFunction.applyAsInt(prev);
    } while (!compareAndSet(prev, next));

    //返回舊值
    return prev;
}

1.2.4 updateAndGet(IntUnaryOperator updateFunction)

public final int updateAndGet(IntUnaryOperator updateFunction) {
    int prev, next;
    do {
        prev = get();
        next = updateFunction.applyAsInt(prev);
    } while (!compareAndSet(prev, next));
    return next;
}

updateAndGet方法實現跟getAndUpdate一致,唯一不同的是updateAndGet方法返回的是修改後的值。

1.2.5 getAndAccumulate

public final int getAndAccumulate(int x,
                                  IntBinaryOperator accumulatorFunction) {
    int prev, next;
    do {
        prev = get();
        //將prev和參數x應用於函數式表達式,獲取新值,並循環通過CAS將當前值改爲新值
        next = accumulatorFunction.applyAsInt(prev, x);
    } while (!compareAndSet(prev, next));

    //返回舊值
    return prev;
}

1.2.6 accumulateAndGet

public final int accumulateAndGet(int x,
                                  IntBinaryOperator accumulatorFunction) {
    int prev, next;
    do {
        prev = get();
        //將prev和參數x應用於函數式表達式,獲取新值,並循環通過CAS將當前值改爲新值
        next = accumulatorFunction.applyAsInt(prev, x);
    } while (!compareAndSet(prev, next));

    //返回新值
    return next;
}

1.3 AtomicInteger應用

AtomicInteger最直觀的應用肯定是計數器,其實AtomicInteger的CAS屬性,除了可以實現樂觀非阻塞算法,它也可以用來實現悲觀阻塞式算法,比如鎖。實際上,Java併發包中的所有阻塞式工具、容器、算法也都是基於CAS的。下面代碼展示使用AtomicInteger實現一個鎖,如下:

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。

2. AtomicBoolean/AtomicLong/AtomicReference

AtomicBoolean/AtomicLong/AtomicReference的用法和原理與AtomicInteger非常類似,下面簡單介紹一下。

2.1 AtomicBoolean

AtomicBoolean類聲明如下:

public class AtomicBoolean implements java.io.Serializable {

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicBoolean.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

    //Methods

}   

可以看到AtomicBoolean類內部的值value並不是boolean類型的,而實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);
}

就是把boolean值轉化爲int值,然後通過Unsafe類的compareAndSwapInt方法,以原子方式替換value值。

2.2 AtomicLong

AtomicLong可以用來在程序中生成唯一序列號,它的方法與AtomicInteger類似,就不贅述了。它的CAS方法調用的是unsafe的另一個方法,如:

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

原理是同AtomicInteger一樣的,使用方式也一致,這裏不多講了。

2.3 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);
}

3. 原子數組

原子數組方便以原子的方式更新數組中的每個元素,下面以AtomicIntegerArray爲例來簡要介紹下,AtomicLongArray、AtomicReferenceArray類似。

3.1 構造方法

public AtomicIntegerArray(int length)
public AtomicIntegerArray(int[] array) 

第一個會創建一個長度爲length的空數組;第二個構造函數接受一個已有的數組,但不會直接操作該數組,而是會創建一個新數組,只是拷貝參數數組中的內容到新數組,對AtomicIntegerArray內容的修改不會影響原數組。

3.2 方法說明

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)

含義跟AtomicInteger相同,不同的是AtomicIntegerArray中的方法要上送數組的index,以原子方式修改數組特定index位置的元素。

3.3 使用示例

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

4. AtomicReferenceFieldUpdater

AtomicReferenceFieldUpdater方便以原子方式更新對象中的成員變量,成員變量不需要聲明爲原子變量,AtomicReferenceFieldUpdater是基於反射機制實現的。下面看一個簡單的使用示例:

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());
    }
}

5. AtomicStampedReference

使用CAS方式更新有一個ABA問題,一個線程開始看到的值是A,隨後使用CAS進行更新,它的實際期望是沒有其他線程修改過才更新,但普通的CAS做不到,因爲可能在這個過程中,已經有其他線程修改過了,比如先改爲了B,然後又改回爲了A。

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

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)));
}

private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

AtomicStampedReference在compareAndSet中要同時修改兩個值,一個是引用,另一個是時間戳,通過上述代碼,可以看到,AtomicStampedReference內部會將這兩個值封裝爲Pair對象,並通過UNSAFE.compareAndSwapObject以原子方式更新。

使用示例:

public static void main(String[] args) {
    Pair<Integer, Integer> 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);
}

6. AtomicMarkableReference

AtomicMarkableReference是另一個AtomicReference的增強類,與AtomicStampedReference類似,它也是給引用關聯了一個字段,只是這次是一個boolean類型的標誌位,只有引用值和標誌位都相同的情況下才進行修改,同樣可以解決ABA問題。來看一下CAS實現:

public boolean compareAndSet(V       expectedReference,
                             V       newReference,
                             boolean expectedMark,
                             boolean newMark) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedMark == current.mark &&
        ((newReference == current.reference &&
          newMark == current.mark) ||
         casPair(current, Pair.of(newReference, newMark)));
}

private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

可以看到跟AtomicStampedReference實現方式一致,唯一的區別是標誌位不同,AtomicStampedReference中使用的是int型變量,AtomicMarkableReference中使用的是boolean變量。

以上就是原子變量的所有內容,原子變量可以保證在無鎖的環境下原子的改變變量,但是也存在一些侷限性,如下:

  • ABA問題:普通的CAS操作檢查值有沒有發生變化,如果沒發生變化則更新,但是如果一個值一開始是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號,在變量前追加上版本號,每次更新的時候把版本號加1,那麼A -> B -> A就會編程1A -> 2B -> 3A。Java API atomic包內的AtomicStampedReference和AtomicMarkableReference都使用類似的思路解決了ABA問題。
  • 循環時間長開銷大:類似於incrementAndGet方法都使用了自旋CAS,如果自旋長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支持處理器提供的pause指令,那麼效率會有一定的提升。pause指令有兩個作用:第一,它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,有些處理器上延遲時間是零;第二,他可以避免在退出循環的時候因內存順序衝突(Memory Order Violantion)而引起CPU流水線被清空(CPU Pipeline Flush),從而提高CPU執行效率。
  • 只能保證一個共享變量的原子操作:當對一個共享變量執行操作時,我們可以使用CAS方式保證原子性,但是對多個共享變量操作時,就無法保證操作的原子性了,這時候還是需要使用鎖。還有一個解決辦法就是,把多個共享變量合成一個共享變量,然後通過AtomicReference類來原子更新合併的共享變量。

參考鏈接:

1. 《Java編程的邏輯》

2. Java API

3. 《Java併發編程的藝術》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章