天天看點

JUC(17)java中有哪些原子操作

文章目錄

  • ​​一、什麼是原子操作​​
  • ​​二、Java中原子操作的實作方式​​
  • ​​2.1使用鎖實作原子操作​​
  • ​​2.2使用CAS實作原子操作​​
  • ​​2.2.1 CAS實作原子操作的問題​​
  • ​​三、CPU如何實作原子操作​​
  • ​​3.1對于單核CPU​​
  • ​​3.2對于多核CPU​​

一、什麼是原子操作

  • 原子操作:一個或多個操作在CPU執行過程中不被中斷的特性
  • 當我們說原子操作時,需要厘清楚針對的是CPU指令級别還是進階語言級别。
  • 比如:經典的銀行轉賬場景,是語言級别的原子操作;而當我們說volatile修飾的變量的複合操作,其原子性不能被保證(這裡指的是CPU指令級别)。二者的本質是一緻的。
  • “原子操作”的實質其實并不是指“不可分割”,這隻是外在表現,本質在于多個資源之間有一緻性的要求,操作的中間态對外不可見。
  • 比如:在32位機器上寫64位的long變量有中間狀态(隻寫了64位中的32位);銀行轉賬操作中也有中間狀态(A向B轉賬,A扣錢了,B還沒來得及加錢)

二、Java中原子操作的實作方式

  • 除了long和double之外的基本類型的指派操作,因為long和double類型是64位的,是以它們的操作在32位機器上不算原子操作,而在64位的機器上是原子操作。
  • 所有引用reference的指派操作
  • java.concurrent.Atomic *包中所有類的原子操作
  • Java使用鎖和自旋CAS實作原子操作

2.1使用鎖實作原子操作

  • 鎖機制保證隻有拿到鎖的線程才能操作鎖定的記憶體區域。
  • JVM内部實作了多種鎖,偏向鎖、輕量鎖、互斥鎖。不過輕量鎖、互斥鎖(即不包括偏向鎖),實作鎖時還是使用了CAS,即:一個線程進入同步代碼時用自CAS拿鎖,退出塊的時候用CAS釋放鎖。
  • ​synchronized​

    ​鎖定的臨界區代碼對共享變量的操作是原子操作。

2.2使用CAS實作原子操作

  • 利用CAS實作原子操作,其實我們在用的時候,是使用java.util.concurrent.atomic包下的各種原子類,這些原子類裡面的各種方法底層使用的就是CAS。下面是該包中的類:
  • AtomicBoolean – 原子布爾
  • AtomicInteger – 原子整型
  • AtomicIntegerArray – 原子整型數組
  • AtomicLong – 原子長整型
  • AtomicLongArray – 原子長整型數組
  • AtomicReference – 原子引用
  • AtomicReferenceArray – 原子引用數組
  • AtomicMarkableReference – 原子标記引用
  • AtomicStampedReference – 原子戳記引用
  • AtomicIntegerFieldUpdater – 用來包裹對整形 volatile 域的原子操作
  • AtomicLongFieldUpdater – 用來包裹對長整型 volatile 域的原子操作
  • AtomicReferenceFieldUpdater – 用來包裹對對象 volatile 域的原子操作
  • AtomicBoolean – 原子布爾
  • AtomicInteger – 原子整型
  • AtomicIntegerArray – 原子整型數組
  • AtomicLong – 原子長整型
  • AtomicLongArray – 原子長整型數組
  • AtomicReference – 原子引用
  • AtomicReferenceArray – 原子引用數組
  • AtomicMarkableReference – 原子标記引用
  • AtomicStampedReference – 原子戳記引用
  • AtomicIntegerFieldUpdater – 用來包裹對整形 volatile 域的原子操作
  • AtomicLongFieldUpdater – 用來包裹對長整型 volatile 域的原子操作
  • AtomicReferenceFieldUpdater – 用來包裹對對象 volatile 域的原子操作
  • 在這一點可以參考:
  • 一個案例:
package com.wlw.cas;

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo {
    //CAS :compareAndSet() 這個方法的縮寫 比較并交換!
    public static void main(String[] args) {
        //原子類的底層運用了CAS
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        // public final boolean compareAndSet(int expect, int update)
        //如果我期望的值達到了,那麼就更新,否則,就不更新,CAS是CPU的并發原語!
        System.out.println(atomicInteger.compareAndSet(2020, 2021)); //true
        System.out.println(atomicInteger.get()); //2021 ,atomicInteger的值更新到了2021

        System.out.println(atomicInteger.compareAndSet(2020, 2021)); //false
        System.out.println(atomicInteger.get()); //2021,此時atomicInteger的值是2021,不更新
    }
}      

2.2.1 CAS實作原子操作的問題

CAS是并發包的基石,但用CAS有三個問題:

  • ABA問題

    根源:CAS的本質是對變量的current value ,期望值 expected value 進行比較,二者相等時,再将 給定值 given update value 設為目前值。

    是以會存在一種場景,變量值原來是A,變成了B,又變成了A,使用CAS檢查時會發現值并未變化,實際上是變化了。

    對于數值類型的變量,比如int,這種問題關系不大,但對于引用類型,則會産生很大影響。

    ABA問題解決思路:版本号。在變量前加版本号,每次變量更新時将版本号加1,A -> B -> A,就變成 1A -> 2B -> 3A。

    JDK5之後Atomic包中提供了AtomicStampedReference#compareAndSet來解決ABA問題。

public boolean compareAndSet(@Nullable V expectedReference,
                         V newReference,
                         int expectedStamp,
                         int newStamp)      
  • 循環時間長則開銷大

    自旋CAS若長時間不成功,會對CPU造成較大開銷。不過有的JVM可支援CPU的pause指令的話,效率可有一定提升。

    pause作用:

  • 延遲流水線指令(de-pipeline),使CPU不至于消耗過多執行資源。
  • 可避免退出循環時因記憶體順序沖突(memorey order violation )引起CPU流水線被清空(CPU pipeline flush),進而提高CPU的執行效率。
  • 隻能保證一個共享變量的原子操作

    CAS隻能對單個共享變量如是操作,對多個共享變量操作時則無法保證原子性,此時可以用鎖。

    另外,也可“取巧”,将多個共享變量合成一個共享變量來操作。比如a=2,b=t,合并起來ab=2t,然後用CAS操作ab。JDK5提供​

    ​AtomicReference​

    ​保證引用對象間的原子性,它可将多個變量放在一個對象中來進行CAS操作。

三、CPU如何實作原子操作

3.1對于單核CPU

  • 對于單核cpu,所有的事件都是串行,執行完第一才會去執行第二個。是以,單核CPU實作原子操作比較簡單。
  • 在單核CPU中,每個指令都保證是原子的,即中斷隻會在指令之間發生。Intel x86指令集支援記憶體操作數的inc操作,将多條指令的操作在一條指令内完成。因為程序的上下文切換是在總是在一條指令執行完成後,是以不會寫撕裂或者讀撕裂等并發問題。

3.2對于多核CPU

  • 首先,CPU會自動保證基本的記憶體操作的原子性。CPU保證從記憶體中讀寫一個位元組是原子的,即:當一個CPU讀一個位元組時,其他處理器不能通路這個位元組的記憶體位址。
  • 但對于複雜的記憶體操作如跨總線跨度、跨多個緩存行的通路,CPU是不能自動保證的。不過,CPU提供總線鎖定和緩存鎖定。

1、使用總線鎖

  • 總線鎖用來鎖住某一個共享記憶體。當一個cpu要對記憶體進行操作時,會加上總線鎖,限制其他cpu對共享記憶體操作。Intel x86指令集提供了指令字首lock用于鎖定前端串行總線(FSB),保證了指令執行時不會受到其他處理器的幹擾。
  • 假如多個處理器同時讀改寫共享變量,這種操作(e.g. i++)不是原子的,操作完的共享變量的值會和期望的不一緻。
  • 原因:多個處理器同時從各自緩存讀i,分别 + 1,分别寫入記憶體。要想保證讀改寫共享變量的原子性,必須保證CPU1讀改寫該變量時,CPU2不能操作緩存了該變量記憶體位址的緩存。
  • 總線鎖就是解決此問題的。總線鎖:利用LOCK#信号,當一個CPU在總線上輸出此信号,其他CPU的請求會被阻塞,則該CPU可以獨占共享記憶體。
  • 使用總線鎖,會鎖定cpu與記憶體的通信,是以開銷很大。有的cpu架構提供開銷更小的緩存鎖。緩存鎖在一個cpu進行回寫時,會使用緩存一緻性機制來保護内部記憶體,當其他處理器回寫已被鎖定的緩存行的資料時會使緩存行無效。
  • 同一時刻,其實隻要保證對某個記憶體位址的操作是原子的即可,但總線鎖定把CPU和記憶體間的通信鎖住了。鎖定期間,其他CPU不能操作其他記憶體位址的資料,是以總線鎖定的開銷比較大。目前CPU會在一些場景下使用緩存鎖替代總線鎖來優化。
  • 頻繁使用的記憶體會被緩存到L1、L2、L3高速cache中,原子操作可直接在高速cache中進行,不需要聲明總線鎖。
  • 緩存鎖是指:緩存一緻性機制阻止同時修改由兩個以上CPU緩存的記憶體區域資料,當其他CPU回寫已被鎖定的緩存行資料時,會使緩存行無效。
  • CAS(Compare and Swap),cas記錄原來記憶體中的值old,和将要修改的值new。CAS會檢測現在記憶體中的值now,如果now和old一緻,則說明沒有别的cpu進行了記憶體修改,執行new值的更新。如果new和old值不等,則說明值已被修改,丢棄new值。

繼續閱讀