還在用Synchronized?Atomic你瞭解不?

前言

只有光頭才能變強java

以前已經寫過多線程相關的文章了,有興趣的同窗能夠去了解一下:git

多線程文章

在閱讀《阿里巴巴 Java開發手冊》讀後感時,還有未解決的問題:github

若是是count++操做,使用以下類實現: AtomicInteger count = new AtomicInteger(); count.addAndGet(1);若是是 JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減小樂觀鎖的重試次數)。算法

以前在學習的時候也看過AtomicInteger類不少次了,一直沒有去作相關的筆記。如今遇到問題了,因而就過來寫寫筆記,並但願在學習的過程當中解決掉問題編程

1、基礎鋪墊

首先咱們來個例子:數組

public class AtomicMain {

    public static void main(String[] args) throws InterruptedException {

        ExecutorService service = Executors.newCachedThreadPool();

        Count count = new Count();
        // 100個線程對共享變量進行加1
        for (int i = 0; i < 100; i++) {
            service.execute(() -> count.increase());
        }

        // 等待上述的線程執行完
        service.shutdown();
        service.awaitTermination(1, TimeUnit.DAYS);


        System.out.println("公衆號:Java3y---------");
        System.out.println(count.getCount());
    }

}

class Count{

    // 共享變量
    private Integer count = 0;
    public Integer getCount() {
        return count;
    }
    public void increase() {
        count++;
    }
}
複製代碼

大家猜猜得出的結果是多少?是100嗎?安全

多運行幾回能夠發現:結果是不肯定的,多是95,也多是98,也多是100多線程

結果不肯定

根據結果咱們得知:上面的代碼是線程不安全的!若是線程安全的代碼,屢次執行的結果是一致的!併發

咱們能夠發現問題所在:count++不是原子操做。由於count++須要通過讀取-修改-寫入三個步驟。舉個例子:高併發

  • 若是某一個時刻:線程A讀到count的值是10,線程B讀到count的值也是10
  • 線程A對count++,此時count的值爲11
  • 線程B對count++,此時count的值也是11(由於線程B讀到的count是10)
  • 因此到這裏應該知道爲啥咱們的結果是不肯定了吧。

要將上面的代碼變成線程安全的(每次得出的結果是100),那也很簡單,畢竟咱們是學過synchronized鎖的人:

  • increase()加synchronized鎖就行了
public synchronized void increase() {
    count++;
}
複製代碼

不管執行多少次,得出的都是100:

結果都是100

從上面的代碼咱們也能夠發現,只作一個++這麼簡單的操做,都用到了synchronized鎖,未免有點小題大作了。

  • Synchronized鎖是獨佔的,意味着若是有別的線程在執行,當前線程只能是等待!

因而咱們原子變量的類就登場了!

1.2CAS再來看看

在寫文章以前,本覺得對CAS有必定的瞭解了(由於以前已經看過相關概念,覺得本身理解了)..但真正敲起鍵盤寫的時候,仍是發現沒徹底弄懂...因此再來看看CAS吧。

來源維基百科:

比較並交換(compare and swap, CAS),是原子操做的一種,可用於在多線程編程中實現不被打斷的數據交換操做,從而避免多線程同時改寫某一數據時因爲執行順序不肯定性以及中斷的不可預知性產生的數據不一致問題。 該操做經過將內存中的值與指定數據進行比較,當數值同樣時將內存中的數據替換爲新的值。

CAS有3個操做數:

  • 內存值V
  • 舊的預期值A
  • 要修改的新值B

當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值(A和內存值V相同時,將內存值V修改成B),而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試(或者什麼都不作)

咱們畫張圖來理解一下:

CAS理解

咱們能夠發現CAS有兩種狀況:

  • 若是內存值V和咱們的預期值A相等,則將內存值修改成B,操做成功!
  • 若是內存值V和咱們的預期值A不相等,通常也有兩種狀況:
    • 重試(自旋)
    • 什麼都不作

咱們再繼續往下看,若是內存值V和咱們的預期值A不相等時,應該何時重試,何時什麼都不作。

1.2.1CAS失敗重試(自旋)

好比說,我上面用了100個線程,對count值進行加1。咱們都知道:若是在線程安全的狀況下,這個count值最終的結果必定是爲100的。那就意味着:每一個線程都會對這個count值實質地進行加1

我繼續畫張圖來講明一下CAS是如何重試(循環再試)的:

CAS循環重試

上面圖只模擬出兩個線程的狀況,但足夠說明問題了。

1.2.2CAS失敗什麼都不作

上面是每一個線程都要爲count值加1,但咱們也能夠有這種狀況:將count值設置爲5

我也來畫個圖說明一下:

CAS失敗什麼都不作

理解CAS的核心就是:CAS是原子性的,雖然你可能看到比較後再修改(compare and swap)以爲會有兩個操做,但終究是原子性的!

2、原子變量類簡單介紹

原子變量類在java.util.concurrent.atomic包下,整體來看有這麼多個:

原子變量類

咱們能夠對其進行分類:

  • 基本類型:
    • AtomicBoolean:布爾型
    • AtomicInteger:整型
    • AtomicLong:長整型
  • 數組:
    • AtomicIntegerArray:數組裏的整型
    • AtomicLongArray:數組裏的長整型
    • AtomicReferenceArray:數組裏的引用類型
  • 引用類型:
    • AtomicReference:引用類型
    • AtomicStampedReference:帶有版本號的引用類型
    • AtomicMarkableReference:帶有標記位的引用類型
  • 對象的屬性:
    • AtomicIntegerFieldUpdater:對象的屬性是整型
    • AtomicLongFieldUpdater:對象的屬性是長整型
    • AtomicReferenceFieldUpdater:對象的屬性是引用類型
  • JDK8新增DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder
    • 是對AtomicLong等類的改進。好比LongAccumulator與LongAdder在高併發環境下比AtomicLong更高效。

Atomic包裏的類基本都是使用Unsafe實現的包裝類。

Unsafe裏邊有幾個咱們喜歡的方法(CAS):

// 第一和第二個參數表明對象的實例以及地址,第三個參數表明指望值,第四個參數表明更新值
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

複製代碼

從原理上概述就是:Atomic包的類的實現絕大調用Unsafe的方法,而Unsafe底層其實是調用C代碼,C代碼調用匯編,最後生成出一條CPU指令cmpxchg,完成操做。這也就爲啥CAS是原子性的,由於它是一條CPU指令,不會被打斷。

2.1原子變量類使用

既然咱們上面也說到了,使用Synchronized鎖有點小題大做了,咱們用原子變量類來改一下:

class Count{

    // 共享變量(使用AtomicInteger來替代Synchronized鎖)
    private AtomicInteger count = new AtomicInteger(0);
    
    public Integer getCount() {
        return count.get();
    }
    public void increase() {
        count.incrementAndGet();
    }
}


// Main方法仍是如上

複製代碼

修改完,不管執行多少次,咱們的結果永遠是100!

其實Atomic包下原子類的使用方式都不會差太多,瞭解原子類各類類型,看看API,基本就會用了(網上也寫得比較詳細,因此我這裏果斷偷懶了)...

2.2ABA問題

使用CAS有個缺點就是ABA的問題,什麼是ABA問題呢?首先我用文字描述一下:

  • 如今我有一個變量count=10,如今有三個線程,分別爲A、B、C
  • 線程A和線程C同時讀到count變量,因此線程A和線程C的內存值和預期值都爲10
  • 此時線程A使用CAS將count值修改爲100
  • 修改完後,就在這時,線程B進來了,讀取獲得count的值爲100(內存值和預期值都是100),將count值修改爲10
  • 線程C拿到執行權,發現內存值是10,預期值也是10,將count值修改爲11

上面的操做均可以正常執行完的,這樣會發生什麼問題呢??線程C沒法得知線程A和線程B修改過的count值,這樣是有風險的。

下面我再畫個圖來講明一下ABA的問題(以鏈表爲例):

CAS ABA的問題講解

2.3解決ABA問題

要解決ABA的問題,咱們可使用JDK給咱們提供的AtomicStampedReference和AtomicMarkableReference類。

AtomicStampedReference:

An {@code AtomicStampedReference} maintains an object referencealong with an integer "stamp", that can be updated atomically.

簡單來講就是在給爲這個對象提供了一個版本,而且這個版本若是被修改了,是自動更新的。

原理大概就是:維護了一個Pair對象,Pair對象存儲咱們的對象引用和一個stamp值。每次CAS比較的是兩個Pair對象

// Pair對象
    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;

	// 比較的是Pari對象
    public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

複製代碼

由於多了一個版本號比較,因此就不會存在ABA的問題了。

2.4LongAdder性能比AtomicLong要好

若是是 JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減小樂觀鎖的重試次數)。

去查閱了一些博客和資料,大概的意思就是:

  • 使用AtomicLong時,在高併發下大量線程會同時去競爭更新同一個原子變量,可是因爲同時只有一個線程的CAS會成功,因此其餘線程會不斷嘗試自旋嘗試CAS操做,這會浪費很多的CPU資源。
  • 而LongAdder能夠歸納成這樣:內部核心數據value分離成一個數組(Cell),每一個線程訪問時,經過哈希等算法映射到其中一個數字進行計數,而最終的計數結果,則爲這個數組的求和累加
    • 簡單來講就是將一個值分散成多個值,在併發的時候就能夠分散壓力,性能有所提升。

參考資料:

最後

參考資料:

若是你以爲我寫得還不錯,瞭解一下:

  • 堅持原創的技術公衆號:Java3y。回覆 1 加入Java交流羣
  • 文章的目錄導航(精美腦圖+海量視頻資源):github.com/ZhongFuChen…
相關文章
相關標籤/搜索