盤點JAVA中基於CAS實現的原子類, 你知道哪些?

2022-12-06 12:00:15

前言

JDK中提供了一系列的基於CAS實現的原子類,CAS 的全稱是Compare-And-Swap,底層是lock cmpxchg指令,可以在單核和多核 CPU 下都能夠保證比較交換的原子性。所以說,這些原子類都是執行緒安全的,而且是無鎖並行,執行緒不會頻繁上下文切換,所以在某些場景下效能是優於加鎖。

本文就盤點一下JDK中的原子類,方便我們後續拿來使用。

基礎原子類

  • AtomicInteger:Integer整數型別的原子操作類
  • AtomicBoolean:Boolean型別的原子操作類
  • AtomicLong:Long型別的原子操作類

這邊以AtomicInteger講解下它的API和用法。

構造方法:

  • public AtomicInteger():初始化一個預設值為 0 的原子型 Integer
  • public AtomicInteger(int initialValue):初始化一個指定值的原子型 Integer

常用API:

  • public final int get(): 獲取 AtomicInteger 的值
  • public final int getAndIncrement(): 以原子方式將當前值加 1,返回的是自增前的值
  • public final int incrementAndGet():以原子方式將當前值加 1,返回的是自增後的值
  • public final int getAndSet(int value):以原子方式設定為 newValue 的值,返回舊值
  • public final int addAndGet(int data):以原子方式將輸入的數值與範例中的值相加並返回

使用:

  • 結果1000,大致說明並行情況下保證了執行緒安全

原理分析:

整體實現思路: 自旋(迴圈) + CAS演演算法

  • 當舊的預期值 A == 記憶體值 V 此時可以修改,將 V 改為 B
  • 當舊的預期值 A != 記憶體值 V 此時不能修改,並重新獲取現在的最新值,重新獲取的動作就是自旋
public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
 }
  • valueOffset:偏移量表示該變數值相對於當前物件地址的偏移,Unsafe 就是根據記憶體偏移地址獲取資料

  • 從主記憶體中拷貝到工作記憶體中的值(每次都要從主記憶體拿到最新的值到本地記憶體),然後執行 compareAndSwapInt() 再和主記憶體的值進行比較,假設方法返回 false,那麼就一直執行 while 方法,直到期望的值和真實值一樣,修改資料。

  • 原子類AtomicIntegervalue屬性是volatile型別,保證了多執行緒之間的記憶體可見性,避免執行緒從工作快取中獲取失效的變數。

原子參照

原子參照主要是對物件的原子操作,原子參照類分為AtomicReferenceAtomicStampedReferenceAtomicMarkableReference。它們之間有什麼區別呢?

  1. AtomicReference類

普通的原子類物件

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User user1 = new User("旭陽");

        // 建立原子參照包裝類
        AtomicReference<User> atomicReference = new AtomicReference<>(user1);

        while (true) {
            User user2 = new User("alvin");
            // 比較並交換
            if (atomicReference.compareAndSet(user1, user2)) {
                break;
            }
        }
        System.out.println(atomicReference.get());
    }
}

@Data
@AllArgsConstructor
@ToString
class User {
    private String name;
}
  • 呼叫compareAndSet()方法進行比較替換物件

ABA問題

但是如果使用AtomicReference類,會有一個ABA問題。什麼意思呢?就是一個執行緒將共用變數從A改成B, 後面又改回A, 這是,另外一個執行緒就無法感知這個變化過程,就傻傻的比較,就以為沒有變化,還是一開始的A,就替換了。 實際的確存在這樣只要共用變數發生過變化,就要CAS失敗,有什麼辦法呢?

  1. AtomicStampedReference類

帶版本號的原子類物件

@Slf4j(topic = "a.AtomicStampedReferenceTest")
public class AtomicStampedReferenceTest {
    // 構造AtomicStampedReference
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
    
    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 獲取值 A
        String prev = ref.getReference();
        // 獲取版本號
        int stamp = ref.getStamp();
        log.debug("版本 {}", stamp);
        // 如果中間有其它執行緒干擾,發生了 ABA 現象
        other();
        Thread.sleep(1000);
        // 嘗試改為 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }
    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本為 {}", ref.getStamp());
        }, "t1").start();

        Thread.sleep(500);

        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本為 {}", ref.getStamp());
        }, "t2").start();
    }
}

  • 雖然物件的值變回了A,但是由於版本變了,所以主執行緒CAS失敗
  1. AtomicMarkableReference 類

其實有時候並不關心共用變數修改了幾次,而是隻要標記下是否發生過更改,是否加個標記即可,所以就有了AtomicMarkableReference類。

@Slf4j(topic = "c.AtomicMarkableReferenceTest")
public class AtomicMarkableReferenceTest {
    // 構造 AtomicMarkableReference, 初始標記為false
    static AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", false);

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        other();
        Thread.sleep(1000);
        // 看看是否發生了變化
        log.debug("change {}", ref.isMarked());
    }
    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
                    false, true));
        }, "t1").start();

        Thread.sleep(500);

        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
                    true, true));
        }, "t2").start();
    }
}

  • 通過呼叫isMarked()方法檢視是否發生變化。

原子陣列

  • AtomicIntegerArray: Integer型別的原子陣列
  • AtomicLongArray:Long型別的原子陣列
  • AtomicReferenceArray:參照型別的原子陣列

直接上例子

public class AtomicIntegerArrayTest {
    public static void main(String[] args) throws Exception{
        AtomicIntegerArray array = new AtomicIntegerArray(10);
        Thread t1 = new Thread(()->{
            int index;
            for(int i=1; i<100000; i++) {
                index = i%10; //範圍0~9
                array.incrementAndGet(index);
            }
        });

        Thread t2 = new Thread(()->{
            int index;
            for(int i=1; i<100000; i++) {
                index = i%10; //範圍0~9
                array.decrementAndGet(index);
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(5 * 1000);
        System.out.println(array.toString());
    }
}

  • 兩個執行緒同時對陣列物件進行加和減的操作,最終結果都是0,說明執行緒安全。

原子欄位更新器

  • AtomicReferenceFieldUpdater
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

利用欄位更新器,可以針對物件的某個域(Field)進行原子操作,只能配合 volatile 修飾的欄位使用,否則會出現異常。

@Data
public class AtomicReferenceFieldUpdaterTest {

    private volatile int age = 10;

    private int age2;

    public static void main(String[] args) {
        AtomicIntegerFieldUpdater integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterTest.class, "age");

        AtomicReferenceFieldUpdaterTest ref = new AtomicReferenceFieldUpdaterTest();
        // 對volatile 的age欄位+1
        integerFieldUpdater.getAndIncrement(ref);
        System.out.println(ref.getAge());

        // 修改 非volatile的age2
        integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterTest.class, "age2");
        integerFieldUpdater.getAndIncrement(ref);
    }
}

  • 原子欄位更新器只能更新volatile欄位,它可以保證可見性,但是無法保證原子性。

原子累加器

原子累加器主要是用來做累加的,相關的類有LongAdderDoubleAdderLongAccumulatorDoubleAccumulator

LongAdder是jdk1.8中引入的,它的效能要比AtomicLong方式好。

LongAddr 類是 LongAccumulator 類的一個特例,只是 LongAccumulator 提供了更強大的功能,可以自定義累加規則,當accumulatorFunction 為 null 時就等價於 LongAddr

這邊做個效能的對比例子。

public class LongAdderTest {

    public static void main(String[] args) {
        System.out.println("LongAdder ...........");
        for (int i = 0; i < 5; i++) {
            addFunc(() -> new LongAdder(), adder -> adder.increment());
        }
        System.out.println("AtomicLong ...........");
        for (int i = 0; i < 5; i++) {
            addFunc(() -> new AtomicLong(), adder -> adder.getAndIncrement());
        }
    }

    private static <T> void addFunc(Supplier<T> adderSupplier, Consumer<T> action) {
        T adder = adderSupplier.get();
        long start = System.nanoTime();
        List<Thread> ts = new ArrayList<>();

        // 40個執行緒,每人累加 50 萬
        for (int i = 0; i < 40; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 500000; j++) {
                    action.accept(adder);
                }
            }));
        }
        ts.forEach(t -> t.start());
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(adder + " cost:" + (end - start)/1000_000);
    }
}

主要是由於LongAdder會設定多個累加單元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最後將結果彙總。這樣它們在累加時操作的不同的 Cell 變數,因此減少了 CAS 重試失敗,從而提高效能。

總結

本文總結了JDK中提供的各種原子類,包括基礎原子類、原子參照類、原子陣列類、原子欄位更新器和原子累加器等。有時候,使用這些原子類的效能是比加鎖要高的,特別是在讀多寫少的場景下。但是,不知道大家發現沒有,所有的原子類操作對於一個共用變數執行操作是原子的,如果對於多個共用變數操作時,迴圈 CAS 就無法保證操作的原子性,還是老老實實加鎖吧。

如果本文對你有幫助的話,請留下一個贊吧
更多技術幹活和學習資料盡在個人公眾號——JAVA旭陽