Java1.8-Atomic包簡介(二)

概述

??看過JDK源碼的童鞋都知道,Java的concurrent包下又包含了兩個包:locks和atomic,locks包下主要是實現線程安全的Lock類相關的接口與實現,而atomic包則是Java中原子類相關的實現類,今天就來學習下這個包下所包含的類。

介紹

1. 介紹下原子性

??所謂Atomic,翻譯過來就是原子,學習數據庫的時候我們了解過原子。原子被認為是操作中最小的單位,一段代碼如果是原子的,則表示這段代碼在執行過程中,要么執行成功,要么執行失敗,不會有中間狀態。原子操作一般都是底層通過CPU的指令來實現。而atomic包下的這些類,則可以讓我們在多線程環境下,通過一種無鎖的原子操作來實現線程安全。

2. 實現基礎

??atomic包下的類基本上都是借助Unsafe類,通過CAS操作來封裝實現的。Unsafe這個類簡單說下,這個類不屬于Java標準,或者說這個類是Java預留的一個后門類,JDK中,有關提升性能的concurrent或者NIO等操作,大部分都是借助于這個類來封裝操作的。
??我們都知道,Java這種編譯型語言,不像C語言能支持操作內存,正常情況下都是由JVM進行內存的創建回收等操作,但這個類提供了一些直接操作內存相關的底層操作,使得我們也可以手動操作內存,但從類的名字就可以看出,這個類不是安全的,官方也是不建議我們使用的,說不定哪天有可能廢棄掉。

3. 類介紹

在JDK8的atomic包下,大概有16個類,按照原子的更新方式,大概可以分為4類:原子更新普通類型,原子更新數組,原子更新引用,原子更新字段。接下來我們來挨個看下。

3.1 更新基本類型

??atomic包下提供了三種基本類型的原子更新,分別是AtomicBooleanAtomicIntegerAtomicLong,這幾個原子類對應于基礎類型的布爾,整形,長整形,至于Java中其他的基本類型,如float等,如果需要,我們可以參考這幾個類的源碼自行實現。首先,我們來看下AtomicInteger這個類的常用方法:

// 以原子方式遞增給定的值,也就是將給定的值與現有值進行相加
public final int addAndGet(int delta)
// 原子更新,如果當前值等于預期值,則以原子方式將該值設置為輸入的值(update)
public final boolean compareAndSet(int expect, int update)

// 以原子方式遞增,將當前值加1,相當于i++,返回的是自增前的值
public final int getAndIncrement()
// 以原子方式遞減,將當前值減1,相當于i--,返回的是遞減前的值
public final int getAndDecrement()
// 以原子方式遞增,相當于++i,返回的是自增后的值
public final int incrementAndGet()
// 以原子方式遞減,相當于--1,返回的是遞減后的值
public final int decrementAndGet()
// 返回當前值
public final int get()
// 設置當前值為給定的值
public final void set(int newValue)
// 顧名思義,設置并返回舊值
public final int getAndSet(int newValue)
// 添加給定值,并返回舊值
public final int getAndAdd(int delta)
// 添加給定值,返回新的值
public final int addAndGet(int delta)

// JDK8引入,設置并返回舊值,設置的值是使用一個函數式表達式的結果
public final int getAndUpdate(IntUnaryOperator updateFunction)
// 和上面一樣,返回新的值
public final int updateAndGet(IntUnaryOperator updateFunction)

// 更新,返回舊的值,更新值為 當前值與給定值通過表達式進行計算的結果
public final int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction)
// 和上面類似,返回新的值
public final int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction)

// 設置值,以一種延遲的方式;設置值后,不保證該值的修改能被其他線程立刻看到;
// 也就是說,其他線程有可能讀到的還是舊的值;
public final void lazySet(int newValue)

這里面的方法都比較正常,除了兩個方法,一個是lazySet方法,有關這個方法的介紹可以參考:AtomicLong.lazySet是如何工作的?,另一個方法是weakCompareAndSet,這個方法的實現和compareAndSet的方法一模一樣,暫時不知道是做什么用的。

另外,簡單提供一個小例子來測試下:

AtomicInteger atomicInteger = new AtomicInteger(1);
System.out.println(atomicInteger.incrementAndGet());                       // 2
System.out.println(atomicInteger.getAndIncrement());                       // 2
System.out.println(atomicInteger.getAndAccumulate(2, (i, j) -> i + j));    // 3
System.out.println(atomicInteger.get());                                   // 5
System.out.println(atomicInteger.addAndGet(5));                            // 10

而有關AtomicBoolean這個類的操作就更簡單了,這個類中的方法不多,基本上上面都介紹了,而內部的計算則是先將布爾轉換為數字0/1,然后再進行后續計算。而AtomicLong則是和AtomicInteger一樣,就不多說了。

3.2 更新數組

??atomic包下提供了三種數組相關類型的原子更新,分別是 AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray,對應于整型,長整形,引用類型,要說明的一點是,這里說的更新是指更新數組中的某一個元素的操作。由于方法和更新基本類型方法相同,這里只簡單看下AtomicIntegerArray這個類的幾個方法,其他的方法類似。

// i代表數組的下標
public final boolean compareAndSet(int i, int expect, int update)

// 遞增對應下標處的值
public final int incrementAndGet(int i)
public final int decrementAndGet(int i)

AtomicInteger相比,方法多了一個數組下標的參數,其他的都相同,方法內部調用也是一樣的,舉個簡單的例子來看下:

AtomicIntegerArray array = new AtomicIntegerArray(5);
array.set(0, 1);                                        // 設置數組第一個值為1
System.out.println(array.getAndDecrement(0));         // 1
System.out.println(array.addAndGet(0, 5));      // 5

至于AtomicReferenceArray,是一個支持泛型的原子類,操作是相似的:

AtomicReferenceArray<String> array = new AtomicReferenceArray<>(5);
array.set(0, "hello");
System.out.println(array.getAndSet(0, "hello world")); //  hello
System.out.println(array.get(0));                                  //  hello world
3.3 更新引用類型

??更新引用類型的原子類包含了AtomicReference(更新引用類型),AtomicReferenceFieldUpdater(抽象類,更新引用類型里的字段),AtomicMarkableReference(更新帶有標記的引用類型)這三個類,這幾個類能同時更新多個變量。來看下簡單的例子(代碼來自:http://ifeve.com/java-atomic/),首先來看下AtomicReference的使用:

public static void main(String[] args) {
    AtomicReference<User> reference = new AtomicReference<>();
    User user = new User("MrZhang", 20);
    reference.set(user);
    
    User user1 = new User("MrLi", 22);
    // User{name='MrZhang', age=20}
    System.out.println(reference.getAndSet(user1));  
    // User{name='MrLi', age=22}        
    System.out.println(reference.get());
}
static class User {
    private String name;
    private int age;
    // 省略get,set,構造方法,toString方法
}

然后來看下AtomicReferenceFieldUpdater,這個類是個抽象類,用于更新某個實例對象的某一個字段,但該字段有一些限制:

  • 字段的類型必須是volatile類型的,在線程之間共享變量時保證可見性;
  • 只能是實例變量且可修改,不能是類變量,也即是不能是static修飾,也不能是final修飾的變量;
  • 調用者必須能夠直接操作對象的字段,所以對象字段的訪問修飾符需要與調用者保持一致;

該抽象類提供了靜態方法newUpdater來創建它的實現類,該方法接受三個參數:包含該字段的對象的類,要被更新的對象的類,要被更新的字段的名稱,來看一下簡單實現:

public static void main(String[] args) {
    User user = new User("MrZhang", 20);
    AtomicReferenceFieldUpdater<User, String> referenceFieldUpdater =
            AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");

    referenceFieldUpdater.compareAndSet(user, user.name, "MrLi");
    // output : MrLi
    System.out.println(referenceFieldUpdater.get(user));

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

AtomicMarkableReference則是為了解決CAS過程中的ABA問題,AtomicMarkableReference可以讓我們了解到對象在進行CAS的過程中是否被修改過,而與之對應的是AtomicStampedReference,這個類可以給引用加上版本號,讓我們了解到對象都是經過怎樣的修改,以及修改了多少次,這里就不舉例了。

3.4 更新字段類型

??這個其實和上面說的有點相似,如果我們更新的時候只更新對象中的某一個字段,則可以使用atomic包提供的更新字段類型:AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicStampedReference,前兩個顧名思義,就是更新int和long類型,最后一個是更新引用類型,該類提供了版本號,用于解決通過CAS進行原子更新過程中,可能出現的ABA問題。前面這兩個類和上面我們介紹的AtomicReferenceFieldUpdater有些相似,都是抽象類,都需要通過newUpdater方法進行實例化,并且對字段的要求也是一樣的。

public static void main(String[] args) {
    User user = new User("MrZhang", 20);
    AtomicIntegerFieldUpdater<User> updater = 
            AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
    // output : 30
    System.out.println(updater.get(user));
}

static class User {
    volatile String name;
    volatile int age;
}
3.5 JDK8之后引入的類型

??在JDK8之前,針對原子操作,我們基本上可以通過上面提供的這些類來完成我們的多線程下的原子操作,不過在并發高的情況下,上面這些單一的CAS+自旋操作的性能將會是一個問題,所以上述這些類一般用于低并發操作,而針對這個問題,JDK8又引入了下面幾個類:DoubleAdderLongAdderDoubleAccumulatorLongAccumulator,這些類是對AtomicLong這些類的改進與增強,這些類都繼承自Striped64這個類。我們來簡單看下LongAdder這個類,先來看下這個類的方法:

// 將當前值與給定值相加
public void add(long x)
// 自增
public void increment()
// 自減
public void decrement()
// 求和
public long sum()
// 重置
public void reset()
// 上面兩個方法合到一塊
public long sumThenReset()

簡單實例:

LongAdder adder = new LongAdder();
adder.add(12L);
adder.increment();
System.out.println(adder.sum()); // 13

拿LongAdder來說,該類維護了一個數組變量Cell[],這些變量的和就是要以原子方式更新的long型變量,當更新方法add在線程間競爭時,該組變量可以動態增長,從而減少競爭,這種操作類似于分流,也就是分段鎖機制。而方法sum則返回當前在cells變量上的總和。

本文參考自:
《Java并發編程實戰》
Java中的Atomic包使用指南
源碼閱讀:全方位講解LongAdder

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容