死磕 java原子類之終結篇(面試題)

彤哥讀原始碼發表於2019-05-13

概覽

原子操作是指不會被執行緒排程機制打斷的操作,這種操作一旦開始,就一直執行到結束,中間不會有任何執行緒上下文切換。

原子操作可以是一個步驟,也可以是多個操作步驟,但是其順序不可以被打亂,也不可以被切割而只執行其中的一部分,將整個操作視作一個整體是原子性的核心特徵。

在java中提供了很多原子類,筆者在此主要把這些原子類分成四大類。

atomic

原子更新基本型別或引用型別

如果是基本型別,則替換其值,如果是引用,則替換其引用地址,這些類主要有:

(1)AtomicBoolean

原子更新布林型別,內部使用int型別的value儲存1和0表示true和false,底層也是對int型別的原子操作。

(2)AtomicInteger

原子更新int型別。

(3)AtomicLong

原子更新long型別。

(4)AtomicReference

原子更新引用型別,通過泛型指定要操作的類。

(5)AtomicMarkableReference

原子更新引用型別,內部使用Pair承載引用物件及是否被更新過的標記,避免了ABA問題。

(6)AtomicStampedReference

原子更新引用型別,內部使用Pair承載引用物件及更新的郵戳,避免了ABA問題。

這幾個類的操作基本類似,底層都是呼叫Unsafe的compareAndSwapXxx()來實現,基本用法如下:

private static void testAtomicReference() {
    AtomicInteger atomicInteger = new AtomicInteger(1);
    atomicInteger.incrementAndGet();
    atomicInteger.getAndIncrement();
    atomicInteger.compareAndSet(3, 666);
    System.out.println(atomicInteger.get());

    AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);
    atomicStampedReference.compareAndSet(1, 2, 1, 3);
    atomicStampedReference.compareAndSet(2, 666, 3, 5);
    System.out.println(atomicStampedReference.getReference());
    System.out.println(atomicStampedReference.getStamp());
}
複製程式碼

原子更新陣列中的元素

原子更新陣列中的元素,可以更新陣列中指定索引位置的元素,這些類主要有:

(1)AtomicIntegerArray

原子更新int陣列中的元素。

(2)AtomicLongArray

原子更新long陣列中的元素。

(3)AtomicReferenceArray

原子更新Object陣列中的元素。

這幾個類的操作基本類似,更新元素時都要指定在陣列中的索引位置,基本用法如下:

private static void testAtomicReferenceArray() {
    AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
    atomicIntegerArray.getAndIncrement(0);
    atomicIntegerArray.getAndAdd(1, 666);
    atomicIntegerArray.incrementAndGet(2);
    atomicIntegerArray.addAndGet(3, 666);
    atomicIntegerArray.compareAndSet(4, 0, 666);
    
    System.out.println(atomicIntegerArray.get(0));
    System.out.println(atomicIntegerArray.get(1));
    System.out.println(atomicIntegerArray.get(2));
    System.out.println(atomicIntegerArray.get(3));
    System.out.println(atomicIntegerArray.get(4));
    System.out.println(atomicIntegerArray.get(5));
}
複製程式碼

原子更新物件中的欄位

原子更新物件中的欄位,可以更新物件中指定欄位名稱的欄位,這些類主要有:

(1)AtomicIntegerFieldUpdater

原子更新物件中的int型別欄位。

(2)AtomicLongFieldUpdater

原子更新物件中的long型別欄位。

(3)AtomicReferenceFieldUpdater

原子更新物件中的引用型別欄位。

這幾個類的操作基本類似,都需要傳入要更新的欄位名稱,基本用法如下:

private static void testAtomicReferenceField() {
    AtomicReferenceFieldUpdater<User, String> updateName = AtomicReferenceFieldUpdater.newUpdater(User.class, String.class,"name");
    AtomicIntegerFieldUpdater<User> updateAge = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

    User user = new User("tong ge", 21);
    updateName.compareAndSet(user, "tong ge", "read source code");
    updateAge.compareAndSet(user, 21, 25);
    updateAge.incrementAndGet(user);
    
    System.out.println(user);
}

private static class User {
    volatile String name;
    volatile int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "name: " + name + ", age: " + age;
    }
}
複製程式碼

高效能原子類

高效能原子類,是java8中增加的原子類,它們使用分段的思想,把不同的執行緒hash到不同的段上去更新,最後再把這些段的值相加得到最終的值,這些類主要有:

(1)Striped64

下面四個類的父類。

(2)LongAccumulator

long型別的聚合器,需要傳入一個long型別的二元操作,可以用來計算各種聚合操作,包括加乘等。

(3)LongAdder

long型別的累加器,LongAccumulator的特例,只能用來計算加法,且從0開始計算。

(4)DoubleAccumulator

double型別的聚合器,需要傳入一個double型別的二元操作,可以用來計算各種聚合操作,包括加乘等。

(5)DoubleAdder

double型別的累加器,DoubleAccumulator的特例,只能用來計算加法,且從0開始計算。

這幾個類的操作基本類似,其中DoubleAccumulator和DoubleAdder底層其實也是用long來實現的,基本用法如下:

private static void testNewAtomic() {
    LongAdder longAdder = new LongAdder();
    longAdder.increment();
    longAdder.add(666);
    System.out.println(longAdder.sum());

    LongAccumulator longAccumulator = new LongAccumulator((left, right)->left + right * 2, 666);
    longAccumulator.accumulate(1);
    longAccumulator.accumulate(3);
    longAccumulator.accumulate(-4);
    System.out.println(longAccumulator.get());
}
複製程式碼

問題

關於原子類的問題,筆者整理了大概有以下這些:

(1)Unsafe是什麼?

(3)Unsafe為什麼是不安全的?

(4)Unsafe的例項怎麼獲取?

(5)Unsafe的CAS操作?

(6)Unsafe的阻塞/喚醒操作?

(7)Unsafe例項化一個類?

(8)例項化類的六種方式?

(9)原子操作是什麼?

(10)原子操作與資料庫ACID中A的關係?

(11)AtomicInteger怎麼實現原子操作的?

(12)AtomicInteger主要解決了什麼問題?

(13)AtomicInteger有哪些缺點?

(14)ABA是什麼?

(15)ABA的危害?

(16)ABA的解決方法?

(17)AtomicStampedReference是怎麼解決ABA的?

(18)實際工作中遇到過ABA問題嗎?

(19)CPU的快取架構是怎樣的?

(20)CPU的快取行是什麼?

(21)記憶體屏障又是什麼?

(22)偽共享是什麼原因導致的?

(23)怎麼避免偽共享?

(24)消除偽共享在java中的應用?

(25)LongAdder的實現方式?

(26)LongAdder是怎麼消除偽共享的?

(27)LongAdder與AtomicLong的效能對比?

(28)LongAdder中的cells陣列是無限擴容的嗎?

關於原子類的問題差不多就這麼多,都能回答上來嗎?點選下面的連結可以直接到相應的章節檢視:

死磕 java魔法類之Unsafe解析

死磕 java原子類之AtomicInteger原始碼分析

死磕 java原子類之AtomicStampedReference原始碼分析

雜談 什麼是偽共享(false sharing)?

死磕 java原子類之LongAdder原始碼分析

彩蛋

原子類系列原始碼分析到此就結束了,雖然分析的類比較少,但是牽涉的內容非常多,特別是作業系統底層的知識,比如CPU指令、CPU快取架構、記憶體屏障等。

下一章,我們將進入“同步系列”,同步最常見的就是各種鎖了,這裡會著重分析java中的各種鎖、各種同步器以及分散式鎖相關的內容。


歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。

qrcode

相關文章