天天看點

AtomicInteger的實作原理.md1.基本2.AtomaticInteger的應用場景3.源碼分析4.CAS操作的副作用5.AtomicInteger計數怎麼保證重置後的數值準确性

1.基本

AtomicInteger是對Integer類型的一個包裝,提供原子性的通路和更新操作。其原子性的操作是基于CAS實作的。

CAS的過程是這樣,執行運算時,使用目前資料值作為判斷條件,利用CAS指令試圖進行更新。更新之前擷取記憶體中的最新值,與傳來的目前值作比較。如果數值沒有變,則說明沒有其他線程進行并發修改,更新操作成功。則否則要麼進行重試,要麼傳回結果。

2.AtomaticInteger的應用場景

AtomaticInteger最典型的應用場景是計數。比如我們要統計并發插入10萬條資料的耗時,我們需要對插入的資料計數,普通的int變量在多線程環境下的++操作,是線程不安全的,前一個操作可能會被後一個操作所覆寫,是以統計的技術往往小于準确值。這時候就可以使用AtomaticInteger。

使用非常簡單:

private AtomicInteger counter = new AtomicInteger(0);//初始計數為0
// doSomething,執行操作之後,計數即可
int count = counter.incrementAndGet();
           

3.源碼分析

基本屬性

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;
   public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }
           

我們可以看到,AtomicInteger的操作基本都依賴于unsafe提供的底層支援。Unsafe 會利用 value 字段的記憶體位址偏移,完成操作。

進入Unsafe源碼:

public final int getAndSetInt(Object var1, long var2, int var3) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);//目前線程這個時刻,根據AtomicInteger對象和value的記憶體位址偏移,擷取到value值
    } while(!this.compareAndSwapInt(var1, var2, var5,var3));
    //while條件compareAndSwapInt是CAS操作,,如果目前值和執行操作前的最新值一緻,則将value加1,否則操作失敗,傳回false,繼續擷取最新值,直到更新操作成功。
    return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

           

可以看到Unsafe的compareAndSwapInt是用native,native 關鍵字告訴編譯器(其實是JVM)調用的是該方法在外部定義,實際是使用C語言實作的。這裡就不做深究了。

4.CAS操作的副作用

常用的失敗重試機制隐含着一個假設,就是競争情況是短暫的。在多數場景中,重試發生1到2次就成功了,但總有意外情況。是以有需要的時候,考慮自旋的次數,超過多少次之後就不再重試,避免過度消耗CPU.

還有一個就是著名的ABA問題。CAS是在更新時比較前值,如果前值恰好和最新值相同(不是邏輯上的相同),例如期間發生了A->B->A的更新,可能導緻不合理的操作。對于這種情況ava 提供了 AtomicStampedReference類,通過為引用建立版本号的方式,保證CAS的正确性。

5.AtomicInteger計數怎麼保證重置後的數值準确性

假設我們有這樣一個需求,統計每次10w條插入資料的耗時,計數到10w之後,就需要重置為0,先看下代碼:

private AtomicInteger counter = new AtomicInteger(0);//初始計數為0
private long lastTime = 0;
public void insert(){
  //insert
    if(counter.incrementAndGet() == 100000) {
        counter.set(0);
        long currentTime = System.currentTimeMillis();
        log.info("\n\n=============== insert 10w data,time="+ currentTime+",used"+(currentTime-lastTime)+"'s ================\n\n");
        lastTime = currentTime;
  }
}
           

counter.incrementAndGet()的值大于10w時,我們使用set方法,将value值重新置為0。多線程環境下,可能出現多個線程同時執行counter.incrementAndGet()這句代碼(還沒有執行它的傳回值==10w的判斷),第一個線程執行後是99999,不滿足條件,後面幾個線程計數增加到超過10w,而這時執行計數結果是10w那個線程滿足條件(==10w),重置為0,那麼就丢掉了超出10w的幾個計數。計數就不準确了。當然條件是“>=”的時候,計數仍然不準确,而且會執行多次滿足條件後的語句,列印多次日志,這顯然不是我們想要的結果。

有什麼辦法可以實作準确計數呢?有

AtomicInteger提供了一個updateAndGet方法,參數是實作IntUnaryOperator的類。看下它的實作:

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

updateFunction.applyAsInt(prev)這個方法傳回我們希望重置的值。這樣就簡單了,我們隻需要将超出部分的值,從applyAsInt方法傳回就行了。

最後看具體的實作代碼:

private AtomicInteger counter = new AtomicInteger(0);//初始計數為0
private long lastTime = 0;
public void insert(){
  //insert
    if(counter.incrementAndGet() >= 100000) {
        counter.updateAndGet(new CounterVar());
        long currentTime = System.currentTimeMillis();
        log.info("\n\n=============== insert 10w data,time="+ currentTime+",used"+(currentTime-lastTime)+"'s ================\n\n");
        lastTime = currentTime;
  }
}

public class CounterVar implements IntUnaryOperator{
        @Override
        public int applyAsInt(int value) {
            if(value >= 100000) {
                return value-100000;
            }
            return value;
        }

    }