Java Atomic總結

所謂 Atomic,翻譯過來就是原子。原子被認爲是操做中最小的單位,一段代碼若是是原子的,則表示這段代碼在執行過程當中,要麼執行成功,要麼執行失敗。原子操做通常都是底層經過 CPU 的指令來實現。而 atomic 包下的這些類,則可讓咱們在多線程環境下,經過一種無鎖的原子操做來實現線程安全。html

atomic 包下的類基本上都是藉助 Unsafe 類,經過 CAS 操做來封裝實現的。Unsafe 這個類不屬於 Java 標準,或者說這個類是 Java 預留的一個後門類,JDK 中,有關提高性能的 concurrent 或者 NIO 等操做,大部分都是藉助於這個類來封裝操做的。
Java 是種編譯型語言,不像 C 語言能支持操做內存,正常狀況下都是由 JVM 進行內存的建立回收等操做,但這個類提供了一些直接操做內存相關的底層操做,使得咱們也能夠手動操做內存,但從類的名字就能夠看出,這個類不是安全的,官方也是不建議咱們使用的。java

CAS原理

CAS 包含 3 個參數 CAS(V,E,N). V 表示要更新的變量, E 表示預期值, N 表示新值.數據庫

僅當V值等於E值時, 纔會將V的值設爲N, 若是V值和E值不一樣, 則說明已經有其餘線程作了更新, 則當前線程什麼都不作. 最後, CAS返回當前V的真實值. CAS操做是抱着樂觀的態度進行的, 它老是認爲本身能夠成功完成操做.segmentfault

當多個線程同時使用CAS操做一個變量時, 只有一個會勝出, 併成功更新, 其他均會失敗.失敗的線程不會被掛起,僅是被告知失敗, 而且容許再次嘗試, 固然也容許失敗的線程放棄操做.基於這樣的原理, CAS操做即時沒有鎖,也能夠發現其餘線程對當前線程的干擾, 並進行恰當的處理.數組

JDK8atomic 包下,大概有 16 個類,按照原子的更新方式,大概能夠分爲 4 類:原子更新普通類型原子更新數組原子更新引用原子更新字段安全

原子更新普通類型

atomic 包下提供了三種基本類型的原子更新,分別是 AtomicBooleanAtomicIntegerAtomicLong,這幾個原子類對應於基礎類型的布爾,整形,長整形,至於 Java 中其餘的基本類型,如 float 等,若是須要,能夠參考這幾個類的源碼自行實現。多線程

AtomicBoolean

主要接口併發

public final boolean get();
public final boolean compareAndSet(boolean expect, boolean update);
public boolean weakCompareAndSet(boolean expect, boolean update);
public final void set(boolean newValue);
public final void lazySet(boolean newValue);
public final boolean getAndSet(boolean newValue);

這裏面的操做都很正常,主要都是用到了 CAS。這個類中的方法很少,基本上上面都介紹了,而內部的計算則是先將布爾轉換爲數字0/1,而後再進行後續計算。dom

AtomicLong

主要接口高併發

public final long get();
public final void set(long newValue);
public final void lazySet(long newValue);
public final long getAndSet(long newValue);
public final boolean compareAndSet(long expect, long update);
public final boolean weakCompareAndSet(long expect, long update);
public final long getAndIncrement();
public final long getAndDecrement();
public final long getAndAdd(long delta);
public final long incrementAndGet();
public final long decrementAndGet();
public final long addAndGet(long delta);
public final long getAndUpdate(LongUnaryOperator updateFunction);
public final long updateAndGet(LongUnaryOperator updateFunction);

這個和下面要講的 AtomicInteger 相似,下面具體說下。

AtomicInteger

主要接口

// 取得當前值
public final int get();
// 設置當前值
public final void set(int newValue);
// 設置新值,並返回舊值
public final int getAndSet(int newValue);
// 若是當前值爲expect,則設置爲u
public final boolean compareAndSet(int expect, int u);
// 當前值加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);

實現

// 封裝了一個int對其加減
    private volatile int value;
    .......
    public final boolean compareAndSet(int expect, int update) {
    // 經過unsafe 基於CPU的CAS指令來實現, 能夠認爲無阻塞.
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    .......
    public final int getAndIncrement() {
        for (;;) {
        // 當前值
            int current = get();
        // 預期值
            int next = current + 1;
            if (compareAndSet(current, next)) {
        // 若是加成功了, 則返回當前值
                return current;
        }
        // 若是加失敗了, 說明其餘線程已經修改了數據, 與指望不相符,
        // 則繼續無限循環, 直到成功. 這種樂觀鎖, 理論上只要等兩三個時鐘週期就能夠設值成功
        // 相比於直接經過synchronized獨佔鎖的方式操做int, 要大大節約等待時間.
        }
    }

用一個簡單的例子測試下:

AtomicInteger atomicInteger = new AtomicInteger(1);
System.out.println(atomicInteger.incrementAndGet());                       // 2
System.out.println(atomicInteger.getAndIncrement());                       // 2
System.out.println(atomicInteger.getAndAccumulate(2, (i, j) -> i + j));    // 3
System.out.println(atomicInteger.get());                                   // 5
System.out.println(atomicInteger.addAndGet(5));

原子更新數組

atomic 包下提供了三種數組相關類型的原子更新,分別是 AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray,對應於整型,長整形,引用類型,要說明的一點是,這裏說的更新是指更新數組中的某一個元素的操做。

因爲方法和更新基本類型方法相同,這裏只簡單看下 AtomicIntegerArray 這個類的幾個方法,其餘的方法相似。

AtomicIntegerArray

主要接口

// 得到數組第i個下標的元素
public final int get(int i);
// 得到數組的長度
public final int length();
// 將數組第i個下標設置爲newValue,並返回舊的值
public final int getAndSet(int i, int newValue);
// 進行CAS操做,若是第i個下標的元素等於expect,則設置爲update,設置成功返回true
public final boolean compareAndSet(int i, int expect, int update);
// 將第i個下標的元素加1
public final int getAndIncrement(int i);
// 將第i個下標的元素減1
public final int getAndDecrement(int i);
// 將第i個下標的元素增長delta(delta能夠是負數)
public final int getAndAdd(int i, int delta);

實現

// 數組自己基地址
    private static final int base = unsafe.arrayBaseOffset(int[].class);

    // 封裝了一個數組
    private final int[] array;

    static {
        // 數組中對象的寬度, int類型, 4個字節, scale = 4;
        int scale = unsafe.arrayIndexScale(int[].class);
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
        // 前導0 : 一個數字轉爲二進制後, 他前面0的個數
        // 對於4來說, 他就是00000000 00000000 00000000 00000100, 他的前導0 就是29
        // 因此shift = 2
        shift = 31 - Integer.numberOfLeadingZeros(scale);
    }

    // 獲取第i個元素
    public final int get(int i) {
        return getRaw(checkedByteOffset(i));
    }

    // 第i個元素, 在數組中的偏移量是多少
    private long checkedByteOffset(int i) {
        if (i < 0 || i >= array.length)
            throw new IndexOutOfBoundsException("index " + i);

        return byteOffset(i);
    }

    // base : 數組基地址, i << shift, 其實就是i * 4, 由於這邊是int array.
    private static long byteOffset(int i) {
        // i * 4 + base
        return ((long) i << shift) + base;
    }

    // 根據偏移量從數組中獲取數據
    private int getRaw(long offset) {
        return unsafe.getIntVolatile(array, offset);
    }

用一個簡單的例子測試一下:

AtomicIntegerArray array = new AtomicIntegerArray(5);
array.set(0, 1);                                        // 設置數組第一個值爲1
System.out.println(array.getAndDecrement(0));         // 1
System.out.println(array.addAndGet(0, 5));      // 5

原子更新引用

更新引用類型的原子類包含了AtomicReference(更新引用類型),AtomicReferenceFieldUpdater(抽象類,更新引用類型裏的字段),AtomicMarkableReference(更新帶有標記的引用類型)這三個類,這幾個類能同時更新多個變量。

AtomicReference

AtomicInteger 相似, 只是裏面封裝了一個對象, 而不是 int, 對引用進行修改。

主要接口

public final V get();

public final void set(V newValue);

public final boolean compareAndSet(V expect, V update);

public final V getAndSet(V newValue);

測試
使用 10 個線程, 同時嘗試修改 AtomicReference 中的 String, 最終只有一個線程能夠成功。

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceTest {
    public final static AtomicReference<String> attxnicStr = new AtomicReference<String>("abc");

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread() {
                public void run() {
                    try {
                        Thread.sleep(Math.abs((int) (Math.random() * 100)));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (attxnicStr.compareAndSet("abc", "def")) {
                        System.out.println("Thread:" + Thread.currentThread().getId() + " change value to " + attxnicStr.get());
                    } else {
                        System.out.println("Thread:" + Thread.currentThread().getId() + " change failed!");
                    }
                }
            }.start();
        }
    }
}

原子更新字段

若是更新的時候只更新對象中的某一個字段,則可使用 atomic 包提供的更新字段類型:AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicStampedReference,前兩個顧名思義,就是更新 intlong 類型,最後一個是更新引用類型,該類提供了版本號,用於解決經過 CAS 進行原子更新過程當中,可能出現的 ABA 問題。
前面這兩個類和上面介紹的 AtomicReferenceFieldUpdater 有些類似,都是抽象類,都須要經過 newUpdater 方法進行實例化,而且對字段的要求也是同樣的。

AtomicStampedReference

ABA問題

線程一準備用 CAS 將變量的值由 A 替換爲 B, 在此以前線程二將變量的值由 A 替換爲 C, 線程三又將 C 替換爲A, 而後線程一執行 CAS 時發現變量的值仍然爲 A, 因此線程一 CAS 成功.

主要接口

// 比較設置 參數依次爲:指望值 寫入新值 指望時間戳 新時間戳
public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp)
// 得到當前對象引用
public V getReference()
// 得到當前時間戳
public int getStamp()
// 設置當前對象引用和時間戳
public void set(V newReference, int newStamp)

分析

// 內部封裝了一個Pair對象, 每次對對象操做的時候, stamp + 1
    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);
        }
    }

    private volatile Pair<V> pair;

    // 進行cas操做的時候, 會對比stamp的值
    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)));
    }

測試

要求:後臺使用多個線程對用戶充值, 要求只能充值一次.

public class AtomicStampedReferenceDemo {
 static AtomicStampedReference<Integer> money=new AtomicStampedReference<Integer>(19,0);
    public staticvoid main(String[] args) {
        //模擬多個線程同時更新後臺數據庫,爲用戶充值
        for(int i = 0 ; i < 3 ; i++) {
            final int timestamp=money.getStamp();
            newThread() {  
                public void run() { 
                    while(true){
                       while(true){
                           Integerm=money.getReference();
                            if(m<20){
                         if(money.compareAndSet(m,m+20,timestamp,timestamp+1)){
                          System.out.println("餘額小於20元,充值成功,餘額:"+money.getReference()+"元");
                                    break;
                                }
                            }else{
                               //System.out.println("餘額大於20元,無需充值");
                                break ;
                             }
                       }
                    }
                } 
            }.start();
         }
        
       //用戶消費線程,模擬消費行爲
        new Thread() { 
             publicvoid run() { 
                for(int i=0;i<100;i++){
                   while(true){
                        int timestamp=money.getStamp();
                        Integer m=money.getReference();
                        if(m>10){
                             System.out.println("大於10元");
                            if(money.compareAndSet(m, m-10,timestamp,timestamp+1)){
                             System.out.println("成功消費10元,餘額:"+money.getReference());
                                 break;
                             }
                        }else{
                           System.out.println("沒有足夠的金額");
                             break;
                        }
                    }
                    try {Thread.sleep(100);} catch (InterruptedException e) {}
                 }
            } 
        }.start(); 
    }
 }

AtomicIntegerFieldUpdater

可以讓普通變量也可以進行原子操做。

主要接口

public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass,
                                                              String fieldName);

public int incrementAndGet(T obj);
  • Updater只能修改它可見範圍內的變量。由於Updater使用反射獲得這個變量。若是變量不可見,就會出錯。好比若是score申明爲private,就是不可行的。
  • 爲了確保變量被正確的讀取,它必須是volatile類型的。若是咱們原有代碼中未申明這個類型,那麼簡單得申明一下就行。
  • 因爲CAS操做會經過對象實例中的偏移量直接進行賦值,所以,它不支持static字段(Unsafe.objectFieldOffset()不支持靜態變量)。

測試

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicIntegerFieldUpdaterDemo {
    public static class Candidate {
        int id;
        // 若是直接把int改爲atomicinteger, 可能對代碼破壞比較大
        // 所以使用AtomicIntegerFieldUpdater對score進行封裝
        volatile int score;
    }

    // 經過反射實現
    public final static AtomicIntegerFieldUpdater<Candidate> scoreUpdater = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
    // 檢查Updater是否工做正確, allScore的結果應該跟score一致
    public static AtomicInteger allScore = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        final Candidate stu = new Candidate();
        Thread[] t = new Thread[10000];
        for (int i = 0; i < 10000; i++) {
            t[i] = new Thread() {
                public void run() {
                    if (Math.random() > 0.4) {
                        scoreUpdater.incrementAndGet(stu);
                        allScore.incrementAndGet();
                    }
                }
            };
            t[i].start();
        }
        for (int i = 0; i < 10000; i++) {
            t[i].join();
        }

        System.out.println("score=" + stu.score);
        System.out.println("allScore=" + allScore);
    }
}

JDK8以後引入的類型

JDK8以前,針對原子操做,咱們基本上能夠經過上面提供的這些類來完成咱們的多線程下的原子操做,不過在併發高的狀況下,上面這些單一的 CAS + 自旋操做的性能將會是一個問題,因此上述這些類通常用於低併發操做。
而針對這個問題,JDK8又引入了下面幾個類:DoubleAdderLongAdderDoubleAccumulatorLongAccumulator,這些類是對AtomicLong這些類的改進與加強,這些類都繼承自Striped64這個類。

Java Atomic.png

參考博客:
Java高併發之無鎖與Atomic源碼分析

本文由博客一文多發平臺 OpenWrite 發佈!
更多內容請點擊個人博客 沐晨
相關文章
相關標籤/搜索