1. 程式人生 > 實用技巧 >併發程式設計005 --- 原子類的使用和原理

併發程式設計005 --- 原子類的使用和原理

原子操作是指不會被執行緒排程機制打斷的操作,也就是說在原子操作期間,不會出現執行緒上下文切換;

JDK在java.util.concurrent.atomic包中提供了多個原子類,如下:

其中從DoubleAccumulator開始,是JDK1.8提供的採用分段思想的高效能原子類;

在多執行緒場景中,不可避免的會有資料的加減運算,很顯然這些操作不是執行緒安全的;我們可以通過synchronized、Lock等方式保證執行緒安全,但是這些加鎖操作大多數情況下會降低執行效率;另外的一種方法就是採用CAS + 自旋鎖來解決執行緒安全問題

CAS --- Compare And Swap,先去記憶體中獲取當前變數值,如果是預期的值,更新為新的值;

自旋鎖 ---- 當執行緒嘗試獲取鎖失敗時,該執行緒可以阻塞,等待所釋放後OS的排程,還可以執行一個空迴圈,不斷的佔用當前CPU,嘗試獲取鎖,後面一種即是自旋鎖;但是其存在一些弊端,如果自旋鎖執行時間過長,會導致CPU資源長時間被佔用,而且該資源消耗會大於執行緒上下文切換,因此,需要根據場景區分使用諮自旋鎖;

上述原子類底層即採用了CAS + 自旋鎖來解決執行緒安全問題

下面以AtomicInteger的常用方法為例說明下原子類的基本用法:

public static void main(String[] args) {
    final AtomicInteger atomicInteger = new
AtomicInteger(1); System.out.println(atomicInteger.incrementAndGet()); }

毫無疑問,執行結果會是2,進入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 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }

這裡面有個很重要的變數:valueOffset,指的是變數在記憶體中的偏移量;

getAndAddInt方法的流程為,先從主記憶體中讀取變數值,然後呼叫CAS方法,直到記憶體中的變數值為預期值;

CAS演算法的思想就是,先從記憶體中取出某個時刻的值A,在下一個時刻將該值與記憶體中的值相比較,如果沒有變化,則進行更新;那麼在時間差內,其它的執行緒可能將記憶體中的值先由A修改為B,然後再修改為A;原來的執行緒在比較時,發現取值沒有發生變化,進行了更新;這就是CAS的“ABA”問題;

正常的運算場景下,ABA不會引入業務問題,但是某些場景下,ABA會導致問題,比如:

單向連結串列A->B構成的棧,A是棧頂,現在要棧頂更新為B,前提是棧頂為A

1、執行緒1,先獲取棧頂的值,為A;

2、這時執行緒2將A、B出棧,然後在棧中分別入棧D、C、A,此時棧結構如下: A->C->D

3、執行緒1,執行比較,發現棧頂為A,因此更新棧頂為B,但是此時B的next為空,偏離了預期

為了解決ABA問題,java引入了帶“版本號”的原子類AtomicMarkableReference和AtomicStampedReference,如果出現ABA的場景,那麼變數的版本號加1,並且在比較時認為不是同一個值