java基礎總結(五十四)--AtomicInteger使用時注意事項

目錄

 

1來自AtomicInteger類真的是線程安全的嘛?

2關於AtomicInteger原理方面的講解

AtomicInteger

非阻塞同步算法與CAS(Compare and Swap)無鎖算法

非阻塞算法 (nonblocking algorithms)

CAS 操作


1來自AtomicInteger類真的是線程安全的嘛?

public final int incrementAndGet() {
        for (;;) {
            int current = get();//1
            int next = current + 1;//2
            if (compareAndSet(current, next))//3
                return next;
        }
    }
以上是源代碼,這個操作相當於i++

如果有兩個AtomicInteger,i1和i2,i1執行到步驟2,自己棧中的值變成5,沒有執行步驟3,因爲步驟3纔是是寫操作,所以此時i1自己的棧中的值是加1後的5,但是主內存中的值還是4,此時i2通過get()得到當前主內存的值,4,進行加1操作,變成5.

到此,i1和i2在他們各自的棧中都是5,現在他們把5這個值寫回主內存,i1成功了,它執行的是compareAndSet(4, 5);
i2在執行步驟3的時候,執行的也是compareAndSet(4, 5);但是,此時主內存中第一個參數對應的值已經被i1改成5,所以此次操作失敗,i2進入第二個循環,通過步驟1獲得當前值5,然後加1,所以最後i2把值變成了6.

如果i2的線程中代碼是這樣的:
if (i2 == 4)
{
    i2.incrementAndGet();//預期是5,但是實際上是6
}

 

2關於AtomicInteger原理方面的講解

只看了前半截覺得寫很好後半部分沒看哦。來自https://blog.csdn.net/WSYW126/article/details/53979918

AtomicInteger


Java中的AtomicInteger大家應該很熟悉,它是爲了解決多線程訪問Integer變量導致結果不正確所設計的一個基於多線程並且支持原子操作的Integer類。

AtomicInteger內部有一個變量UnSafe:private static final Unsafe unsafe = Unsafe.getUnsafe();

Unsafe類是一個可以執行不安全、容易犯錯的操作的一個特殊類。雖然Unsafe類中所有方法都是public的,但是這個類只能在一些被信任的代碼中使用。Unsafe的源碼可以在這裏看 -> UnSafe源碼。

Unsafe類可以執行以下幾種操作:

分配內存,釋放內存:在方法allocateMemory,reallocateMemory,freeMemory中,有點類似c中的malloc,free方法
可以定位對象的屬性在內存中的位置,可以修改對象的屬性值。使用objectFieldOffset方法
掛起和恢復線程,被封裝在LockSupport類中供使用
CAS操作(CompareAndSwap,比較並交換,是一個原子操作)
AtomicInteger中用的就是Unsafe的CAS操作。

Unsafe中的int類型的CAS操作方法:

public final native boolean compareAndSwapInt(Object o, long offset,
                                                int expected,
                                                int x);


參數o就是要進行cas操作的對象,offset參數是內存位置,expected參數就是期望的值(舊值),x參數是需要更新成的新值。

也就是說,如果我把valueOffset地址的值更新爲2,valueOffset原來的值是1,需要這樣調用:

compareAndSwapInt(this, valueOffset, 1, 2)

執行後valueOffset地址的變爲2或者失敗。分析在後面。

valueOffset字段表示內存位置,可以在AtomicInteger對象中使用unsafe得到:

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


AtomicInteger內部使用變量value表示當前的整型值,這個整型變量還是volatile的,表示內存可見性,一個線程修改value之後保證對其他線程的可見性:

private volatile int value;

AtomicInteger內部還封裝了一下CAS,定義了一個compareAndSet方法,只需要2個參數:

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


不論是addAndGet、incrementAndGet、getAndAdd都是在調用unsafe的getAndAddInt方法,然後對返回值進行+/-delta操作。請注意返回的是未修改之前的值,而不是修改後的值。

getAndAddInt 參數: var1 = this, var2 = valueOffset, var4 = delta

compareAndSwapInt 參數:var1 = this, var2 = valueOffset, var5 = 舊值, var5 + var4 = 要修改成的值

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

        return var5;
}


以addAndGet方法爲例:

getAndAdd調用方法:unsafe.getAndAddInt(this, valueOffset, delta);方法內部使用一個do_while循環,先得到當前的值var5,然後再把當前的值加var4,加完之後使用cas原子操作讓當前值加var4處理正確。當然cas原子操作不一定是成功的,所以做了一個死循環,當cas操作成功的時候返回數據。這裏由於使用了cas原子操作,所以不會出現多線程處理錯誤的問題。比如線程A得到var5爲1,線程B也得到var5爲1;線程A的var5 + var4值爲2,進行cas操作並且成功的時候,將var5修改成了2;這個時候線程B得到var5 + var4值仍舊爲2,當進行cas操作的時候由於內存中的值已經是2,而不是1了;所以cas操作會失敗,下一次循環的時候得到的var5就變成了2;也就不會出現多線程處理問題了。

雖然AtomicInteger中的cas操作可以實現非阻塞的原子操作,但是會產生ABA問題。

ABA問題
CAS看起來很爽,但是會導致“ABA問題”。

CAS算法實現一個重要前提需要取出內存中某時刻的數據,而在下時刻比較並替換,那麼在這個時間差類會導致數據的變化。

比如說一個線程one從內存位置V中取出A,這時候另一個線程two也從內存中取出A,並且two進行了一些操作變成了B,然後two又將V位置的數據變成A,這時候線程one進行CAS操作發現內存中仍然是A,然後one操作成功。儘管線程one的CAS操作成功,但是不代表這個過程就是沒有問題的。如果鏈表的頭在變化了兩次後恢復了原值,但是不代表鏈表就沒有變化。因此前面提到的原子操作AtomicStampedReference/AtomicMarkableReference就很有用了。這允許一對變化的元素進行原子操作。

在運用CAS做Lock-Free操作中有一個經典的ABA問題:

線程1準備用CAS將變量的值由A替換爲B,在此之前,線程2將變量的值由A替換爲C,又由C替換爲A,然後線程1執行CAS時發現變量的值仍然爲A,所以CAS成功。但實際上這時的現場已經和最初不同了,儘管CAS成功,但可能存在潛藏的問題,例如下面的例子:

現有一個用單向鏈表實現的堆棧,棧頂爲A,這時線程T1已經知道A.next爲B,然後希望用CAS將棧頂替換爲B:

head.compareAndSet(A,B);

在T1執行上面這條指令之前,線程T2介入,將A、B出棧,再pushD、C、A,此時堆棧結構如下圖,而對象B此時處於遊離狀態:

此時輪到線程T1執行CAS操作,檢測發現棧頂仍爲A,所以CAS成功,棧頂變爲B,但實際上B.next爲null,所以此時的情況變爲:

其中堆棧中只有B一個元素,C和D組成的鏈表不再存在於堆棧中,平白無故就把C、D丟掉了。

以上就是由於ABA問題帶來的隱患,各種樂觀鎖的實現中通常都會用版本戳version來對記錄或對象標記,避免併發操作帶來的問題,在Java中,AtomicStampedReference也實現了這個作用,它通過包裝[E,Integer]的元組來對對象標記版本戳stamp,從而避免ABA問題,例如下面的代碼分別用AtomicInteger和AtomicStampedReference來對初始值爲100的原子整型變量進行更新,AtomicInteger會成功執行CAS操作,而加上版本戳的AtomicStampedReference對於ABA問題會執行CAS失敗。

非阻塞同步算法與CAS(Compare and Swap)無鎖算法


Java在JDK1.5之前都是靠synchronized關鍵字保證同步的,這種通過使用一致的鎖定協議來協調對共享狀態的訪問,可以確保無論哪個線程持有守護變量的鎖,都採用獨佔的方式來訪問這些變量,如果出現多個線程同時訪問鎖,那第一些線線程將被掛起,當線程恢復執行時,必須等待其它線程執行完他們的時間片以後才能被調度執行,在掛起和恢復執行過程中存在着很大的開銷。鎖還存在着其它一些缺點,當一個線程正在等待鎖時,它不能做任何事。如果一個線程在持有鎖的情況下被延遲執行,那麼所有需要這個鎖的線程都無法執行下去。如果被阻塞的線程優先級高,而持有鎖的線程優先級低,將會導致優先級反轉(Priority Inversion)。

獨佔鎖:是一種悲觀鎖,synchronized就是一種獨佔鎖,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。

樂觀鎖:每次不加鎖,假設沒有衝突去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。

與鎖相比,volatile變量是一和更輕量級的同步機制,因爲在使用這些變量時不會發生上下文切換和線程調度等操作,但是volatile變量也存在一些侷限:不能用於構建原子的複合操作,因此當一個變量依賴舊值時就不能使用volatile變量。

非阻塞算法 (nonblocking algorithms)


一個線程的失敗或者掛起不應該影響其他線程的失敗或掛起的算法。

CAS 操作


樂觀鎖用到的機制就是CAS,Compare and Swap。

CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做。

 

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