Java併發編程:11-併發級別和無鎖類

前言:html

前面的幾篇內容都是關於J.U.C的同步工具類,包括使用時須要注意的地方,以及它們是如何經過AQS來實現的,在解讀源碼的時候,發現常常出現CAS操做,下面咱們來了解一下CAS。java

面試問題git

Q :介紹一下Atomic 原子類及其原理?面試

Q :談談你對CAS的理解?編程

1.併發級別

咱們都知道CAS是無鎖操做,那麼什麼是無鎖?這就要引出併發級別這個概念了,因爲臨界區的緣由,多線程之間的併發必須受到控制,根據控制併發的策略,能夠把併發的級別分類,能夠分爲阻塞、無飢餓、無障礙、無鎖、無等待。bootstrap

1.1 阻塞

難進易出,當臨界區被佔用時,其餘線程沒法繼續執行,必須在臨界區外等待,直至臨界區資源被釋放,才能夠去申請,若是申請到了才能繼續執行,否則還要繼續等待。Java中咱們使用內置鎖synchronized或者顯式鎖ReentrantLock,均可能會使線程阻塞。數組

阻塞的控制方式是悲觀策略,認爲兩個進入臨界區的線程極可能都會對數據作修改,爲了保護共享數據,因此使用加鎖的方式,不管線程是進去讀仍是寫,都讓他們排隊進入臨界區,但實際多是大量的讀操做,極少的寫操做,致使讀操做的效率也被極大的拉低了。安全

1.2 無飢餓

線程是有優先級之分的,線程調度的時候會更傾向於知足優先級高的線程。這樣就會致使資源的不公平分配,優先級高的線程一直在執行,優先級低的線程一直拿不到時間片,就會產生飢餓。舉個例子,ReentrantLock支持公平鎖和非公平鎖,非公平鎖會在加入等待隊列前直接嘗試獲取鎖,並無考慮等待隊列中是否已經有節點在它以前排隊,公平鎖的公平之處在於它會去檢查前面是否有節點,若是有則不嘗試獲取鎖。多線程

1.3 無障礙

易進難出,無障礙是一種最弱的非阻塞調度,多個線程能夠同時進入臨界區,可是在釋放資源時,會判斷是否發生數據競爭,好比A線程讀取數據x,要釋放資源時,系統會判斷當前的臨界區內x值是否發生變化,若是發生變化,則會回滾A線程的操做。併發

相對於阻塞級別的悲觀策略,無障礙級別的調度是一種樂觀策略,它認爲多個進入臨界區的線程頗有可能不會發生衝突,可能都是讀操做。若是檢測到衝突,就進行回滾。若是在衝突密集的狀況下,全部線程可能都不斷回滾本身的操做,使得沒有一個線程能夠走出臨界區,影響系統的正常執行。

經過一個實例能夠很好的理解,線程A修改了x的值,要釋放資源出臨界區時,線程B修改了x的值,系統會回滾線程A的操做,線程B要出臨界區時,線程C又修改了x的值,這下該回滾B的操做了,線程C要出臨界區的時候,以前被回滾的A完成了修改操做,因此C也要被回滾了,此處A打算出臨界區,B又來了,這樣就造成了一個閉環,誰都別想走。

1.4 無鎖

無鎖的並行都是無障礙的,在無鎖的狀況下,全部線程均可以嘗試對臨界區的訪問,可是與無障礙不一樣的是,無鎖的併發保證必然有 一個線程 能在有限步內完成操做,離開臨界區

仍是A、B、C三個線程修改x值的問題,要想打破以前造成的閉環,就必需要有一個線程先出去,經過競爭的方式每次選出一個線程勝出,勝出的能夠釋放臨界區資源。

1.5 無等待

無狀態的前提是無鎖的,要求 全部線程 都必須在有限步內完成,這樣就不會發生飢餓現象。

2.無鎖類

2.1 無鎖類的介紹

爲了方便使用CAS,Java在J.U.C中提供了一個atomic包,裏邊包含一些直接使用CAS操做的線程安全的類。

根據操做的數據類型,能夠分爲如下4類:

基本類型

使用原子的方式更新基本類型

  • AtomicInteger:整型原子類
  • AtomicLong:長整型原子類
  • AtomicBoolean :布爾型原子類

數組類型

使用原子的方式更新數組裏的某個元素

  • AtomicIntegerArray:整型數組原子類
  • AtomicLongArray:長整型數組原子類
  • AtomicReferenceArray :引用類型數組原子類

引用類型

  • AtomicReference:引用類型原子類
  • AtomicStampedReference:支持時間戳的引用類型原子類
  • AtomicMarkableReference :原子更新帶有標記位的引用類型

對象的屬性修改類型

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新長整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用類型裏的字段
  • AtomicStampedReference:原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於解決原子的更新數據和數據的版本號,能夠解決使用 CAS 進行原子更新時可能出現的 ABA 問題。
  • AtomicMarkableReference:原子更新帶有標記的引用類型。該類將 boolean 標記與引用關聯起來,也能夠解決使用 CAS 進行原子更新時可能出現的 ABA 問題。

原子類型累加器

  • LongAdder
  • LongAccumulator
  • DoubleAdder
  • DoubleAccumulator

2.2 AtomicInteger

這個類是atomic包中最經常使用的類,能夠將其看做一個線程安全的Integer,可是對其的修改方式和Integer有所不一樣,必須經過方法來修改Integer的值,方法內部都使用CAS。

CAS(Compare And Swap),它包含三個參數CAS(V,E,N)。V表示要更新的變量,E表示預期值,N表示新值。E值是以前讀取的V值,僅當前內存中V值等於E值時,纔會將V值設置爲新值N。若是V值和E值不一樣,則說明有其餘線程作了更新,當前線程什麼都不作。

多個線程同時使用CAS時操做一個變量時,只有一個會成功並返回true。其餘失敗的線程不會被掛起,只是返回false,被告知失敗,而且容許再次嘗試,或者放棄嘗試。

雖然CAS會先讀取值,而後比較,最後再賦值,可是這整個操做是一個原子操做,由一條CPU指令(cmpxchg指令)完成,經過比較交換指令實現,省去了線程頻繁調度和線程鎖競爭的開銷,因此比基於鎖的方式性能更好,並且還不會發生死鎖。

AtomicInteger 類經常使用方法

public final int get()                                     //獲取當前的值
public final void set(int newValue)                        //設置當前值
public final int getAndSet(int newValue)                //設置新值,返回舊值
public final boolean compareAndSet(int expect,int u)    //若是當前值爲expect,則設置爲u
public final int getAndIncrement()                        //當前值加1,返回舊值
public final int getAndDecrement()                        //當前值減1,返回舊值
public final int getAndAdd(int delta)                     //當前值加delat,返回舊值
public final int addAndGEt(int delta)                     //當前值加delat,返回新值
public final int incrementAndGet()                        //當前值加1,返回新值
public final int decrementAndGet()                       //當前值減1,返回新值

AtomicInteger內部實現

//用於保存Integer的值
    private volatile int value;  
    //CAS修改時value,快速定位到value所在內存位置的偏移量
    private static final long valueOffset;
    //定義了真正執行CAS指令的本地方法
    private static final Unsafe unsafe = Unsafe.getUnsafe();

和AtomicInteger相似的還有其餘基本類型的Atomic類,如AtomicLong、AtomicBoolean。

2.3 AtomicIntegerArray

除了基本類型外,還能夠對數組進行原子操做。

  • AtomicIntegerArray:整形數組原子類
  • AtomicLongArray:長整形數組原子類
  • AtomicReferenceArray :引用類型數組原子類

上面三個類提供的方法幾乎相同,因此咱們這裏以 AtomicIntegerArray 爲例子來介紹。

AtomicIntegerArray 類經常使用方法

public final int get(int i)                         //獲取數組第i個下標元素
public final int getAndSet(int i, int newValue)        //將下標爲i的元素設置爲newValue,返回舊值
public final int getAndIncrement(int i)                //將下標爲i的元素遞增,返回舊值
public final int getAndDecrement(int i)             //將下標爲i的元素遞減,返回舊值
public final int getAndAdd(int delta)                //將下標爲i的元素加上預期的值,返回舊值
boolean compareAndSet(int expect, int update)         //進行CAS操做,第i個下標元素等於expect,則設置爲update,成功返回true
public final void lazySet(int i, int newValue)        
//最終將index=i 位置的元素設置爲newValue,使用 lazySet設置以後可能致使其餘線程在以後的一小段時間內仍是能夠讀到舊的值。

2.4 AtomicReference

與AtomicInteger很是類似,不一樣之處在於AtomicReference對應普通的對象引用。在AtomicReference還須要注意「ABA問題「。

」ABA問題「是CAS在兩次樂觀讀之間,變量被修改成B又被修改成A,看起來好像沒有被修改同樣,若是是數字,其保存的信息就是其數值自己,只要最終改回爲指望值,那麼加法計算就不會出錯,可是對引用而言,中間修改對象的內容,可能會影響CAS判斷當前數據的狀態。

這類問題的根本緣由是對象在修改過程當中丟失了狀態信息,所以,只要記錄對象在修改過程當中的狀態值,就能夠解決這類問題,JDK 1.5 之後的 AtomicStampedReference 就是這麼作的,它內部不只維護對象值,還維護了一個更新時間的時間戳,修改的時候不只要指望值,還要而外傳入時間戳,當其中的value被修改時,同時還會更新時間戳。

public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp)
  • expectedReference:指望值
  • newReference:新值
  • expectedStamp:指望時間戳
  • newStamp:新時間戳

2.5 AtomicIntegerFieldUpdater

若是須要原子更新某個類裏的某個字段時,須要用到對象的屬性修改類型原子類。

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
  • AtomicLongFieldUpdater:原子更新長整形字段的更新器
  • AtomicStampedReference :原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於解決原子的更新數據和數據的版本號,能夠解決使用 CAS 進行原子更新時可能出現的 ABA 問題。

要想原子地更新對象的屬性須要兩步。第一步,由於對象的屬性修改類型原子類都是抽象類,因此每次使用都必須使用靜態方法 newUpdater()建立一個更新器,而且須要設置想要更新的類和屬性。第二步,更新的對象屬性必須使用 public volatile 修飾符。

幾個注意事項:

  • 第一,Updater只能修改它可見範圍內的變量。由於Updater使用反射獲得這個變量。若是變量不可見,就會出錯。好比若是score申明爲private,就是不可行的。
  • 第二,爲了確保變量被正確的讀取,它必須是volatile類型的。若是咱們原有代碼中未申明這個類型,那麼簡單地申明一下就行,這不會引發什麼問題。
  • 第三,因爲CAS操做會經過對象實例中的偏移量直接進行賦值,所以,它不支持static字段(Unsafe. objetrieldofset0不支持靜態變量)

2.6 LongAdder

這個類僅僅用來執行累加操做,相比於原子的基本數據類型,速度更快。

實現原理和ConcurronHashMap相似,採用了熱點分離的思想,將一個long劃分爲多個單元,將併發線程的讀寫操做分發到多個單元上,以保證CAS更新可以成功,取值前須要對各個單元進行求和,返回sum。

考慮到若是併發不高的話,這種作法會損耗系統資源,因此默認會維持一個long,若是發生衝突,則會拆分爲多個單元,而且會動態的擴容。在高併發環境下,LongAdder性能更高,但同時也會消耗更多的空間。

和AtomicInteger相似的使用方式,可是不支持compareAndSet()

public void add(long x)
public void increment()
public void decrement()
public long sum()
public long longValue()
public int intValue()

2.7 Unsafe類

unsafe是sun.misc.Unsafe類型,該類是JDK內部使用的專屬類,主要提供一些用於執行低級別、不安全操做的方法,涉及到指針,如直接訪問系統內存資源、自主管理內存資源等,這些方法在提高Java運行效率、加強Java語言底層資源操做能力方面起到了很大的做用。

33-Unsafe.jpg

如何獲取

JDK的開發人員並不但願咱們使用這個類,Unsafe的靜態方法getUnsafe代碼以下:

public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

根據Java類加載器的工做原理,應用程序的類由App Loader加載,而系統核心類,如rt.jar中的類由Bootstrap類加載器加載。Bootstrap加載器沒有Java對象,所以得到這個類加載器會返回null,因此當一個類的類加載其爲null時,說明它是由Bootstarp加載的,或者是rt.jar中的類。

可是必要的時候咱們仍是能夠獲取到的:

  • 方法一:從getUnsafe方法的使用限制條件出發,經過Java命令行命令-Xbootclasspath/a把調用Unsafe相關方法的類A所在jar包路徑追加到默認的bootstrap路徑中,使得A被引導類加載器加載,從而經過Unsafe.getUnsafe方法安全的獲取Unsafe實例。
  • 方法二:經過反射的方式獲取

    private static Unsafe reflectGetUnsafe() {
        try {
          Field field = Unsafe.class.getDeclaredField("theUnsafe");
          field.setAccessible(true);
          return (Unsafe) field.get(null);
        } catch (Exception e) {
          log.error(e.getMessage(), e);
          return null;
        }
    }

CAS相關

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x)
  • 參數 o:給定的對象。
  • 參數 offset:對象內的偏移量,用來尋找要修改的字段(對象的引用會指向該對象的頭部,偏移量能夠快速定位該字段)。
  • 參數 expected:指望值。
  • 參數 x:要設置的值。

    34-CAS.jpg

public native int getInt(long offset);
public native void putInt(long offset, int x);
public native long objectFieldOffset(Field f);

線程調度

還記得AQS中掛起park()和喚醒unpark()操做嗎,具體調用的是LockSupport類的靜態方法。相比於Thread類提供的 suspend()resume(),推薦使用LockSupport的緣由是,即便unpark在park以前調用,也不會致使線程永久被掛起 ,LockSupport的底層使用的是Unsafe類。

public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

3.總結

無鎖相對於阻塞,性能好,不會出現死鎖,可是由於自旋反覆嘗試,可能會出現活鎖或飢餓問題。

無鎖適用於讀多寫少,衝突較少場景。

使用synchronized同步鎖進行線程阻塞和喚醒切換以及用戶態內核態間的切換操做額外浪費消耗cpu資源;而CAS基於硬件實現,不須要進入內核,不須要切換線程,操做自旋概率較少,所以能夠得到更高的性能。

阻塞適用於寫多,衝突較多的場景。

CAS自旋的機率會比較大,從而浪費更多的CPU資源,效率遠低於synchronized。

原子類只能針對一個共享變量,多個變量仍是須要使用互斥鎖來解決。

Reference

  《Java 併發編程實戰》
  《實戰Java高併發程序設計》
  https://snailclimb.gitee.io/j...
  https://tech.meituan.com/2019...

感謝閱讀

相關文章
相關標籤/搜索