文章目錄
- 一、什麼是原子操作
- 二、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提供
保證引用對象間的原子性,它可将多個變量放在一個對象中來進行CAS操作。AtomicReference
三、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值。