1. 程式人生 > 程式設計 >Java Atomic總結

Java Atomic總結

所謂 Atomic,翻譯過來就是原子。原子被認為是操作中最小的單位,一段程式碼如果是原子的,則表示這段程式碼在執行過程中,要麼執行成功,要麼執行失敗。原子操作一般都是底層通過 CPU 的指令來實現。而 atomic 包下的這些類,則可以讓我們在多執行緒環境下,通過一種無鎖的原子操作來實現執行緒安全。

atomic 包下的類基本上都是藉助 Unsafe 類,通過 CAS 操作來封裝實現的。Unsafe 這個類不屬於 Java 標準,或者說這個類是 Java 預留的一個後門類,JDK 中,有關提升效能的 concurrent 或者 NIO 等操作,大部分都是藉助於這個類來封裝操作的。Java 是種編譯型語言,不像 C

語言能支援操作記憶體,正常情況下都是由 JVM 進行記憶體的建立回收等操作,但這個類提供了一些直接操作記憶體相關的底層操作,使得我們也可以手動操作記憶體,但從類的名字就可以看出,這個類不是安全的,官方也是不建議我們使用的。

CAS原理

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

僅當V值等於E值時,才會將V的值設為N,如果V值和E值不同,則說明已經有其他執行緒做了更新,則當前執行緒什麼都不做. 最後,CAS返回當前V的真實值. CAS操作是抱著樂觀的態度進行的,它總是認為自己可以成功完成操作.

當多個執行緒同時使用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,然後再進行後續計算。

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 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+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 釋出!

更多內容請點選我的部落格沐晨