高併發第三彈:執行緒安全-原子性
執行緒安全性?
執行緒安全性主要體現在三個方面:原子性、可見性、有序性
- 原子性:提供了互斥訪問,同一時刻只能有一個執行緒來對它進行操作
- 可見性:一個執行緒對主記憶體的修改可以及時的被其他執行緒觀察到
- 有序性:一個執行緒觀察其他執行緒中的指令執行順序,由於指令重排序的存在,該觀察結果一般雜亂無序。
本章主要關注一下原子性的方面
說到原子性,一共有兩個方面需要學習一下,一個是JDK中已經提供好的Atomic包,他們均使用了CAS完成執行緒的原子性操作,另一個是使用鎖的機制來處理執行緒之間的原子性。鎖包括:synchronized、Lock
Atomic包中的類與CAS(compareAndSwap):
atomic包下提供了AtomicBoolean/AtomicLong/AtomicInteger三個原子更新基本型別,以AtomicInteger為例,其他兩種基本類似。以下是AtomicInteger囊括的大致方法
- public final int getAndSet(int newValue) //給AtomicInteger設定newValue並返回加oldValue
- public final boolean compareAndSet(int expect, int update) //如果輸入的值和期望值相等就set並返回true/false
-
public final int getAndIncrement() //對AtomicInteger原子的加1並返回當前自增前的value
- public final int getAndDecrement() //對AtomicInteger原子的減1並返回自減之前的的value
- public final int getAndAdd(int delta) //對AtomicInteger原子的加上delta值並返加之前的value
- public final int incrementAndGet() //對AtomicInteger原子的加1並返回加1後的值
- public final int decrementAndGet() //對AtomicInteger原子的減1並返回減1後的值
採用的是incrementAndGet方法,此方法的原始碼中呼叫了一個名為unsafe.getAndAddInt的方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
而getAndAddInt方法的具體實現為:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
在此方法中,方法引數為要操作的物件Object var1、期望底層當前的數值為var2、要修改的數值var4。定義的var5為真正從底層取出來的值。採用do..while迴圈的方式去獲取底層數值並與期望值進行比較,比較成功才將值進行修改。而這個比較再進行修改的方法就是compareAndSwapInt就是我們所說的CAS,它是一系列的介面,比如下面羅列的幾個介面。使用native修飾,是底層的方法。CAS取的是compareAndSwap三個單詞的首字母.
另外,示例程式碼中的count可以理解為JMM中的工作記憶體,而這裡的底層數值即為主記憶體,如果看過我上一篇文章的盆友就能把這一塊的知識點串聯起來了。
AtomicLong 與 LongAdder
LongAdder在AtomicLong的基礎上將單點的更新壓力分散到各個節點,在低併發的時候通過對base的直接更新可以很好的保障和AtomicLong的效能基本保持一致,而在高併發的時候通過分散提高了效能。 缺點是LongAdder在統計的時候如果有併發更新,可能導致統計的資料有誤差。
AtomicBoolean
這個類中值得一提的是它包含了一個名為compareAndSet的方法,這個方法可以做到的是控制一個boolean變數在一件事情執行之前為false,事情執行之後變為true。或者也可以理解為可以控制某一件事只讓一個執行緒執行,並僅能執行一次。
AtomicIntegerFieldUpdater
這個類的核心作用是要更新一個指定的類的某一個欄位的值。並且這個欄位一定要用volatile修飾同時還不能是static的。
ABA問題
AtomicStampReference與CAS的ABA問題
ABA : 其實 比如 A想變成B,那麼記憶體中的預期值就A,過去看還真是A,然後就交換;但是記憶體中的 A有可能是別的執行緒進行修改了的.A-->C-->D-->A.這種就和最初的設計思想不符.那麼就加入了AtomicStampReference。
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; private boolean casPair(Pair<V> cmp, Pair<V> val) { return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); } 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))); }
AtomicStampReference的處理思想是,每次變數更新的時候,將變數的版本號+1,之前的ABA問題中,變數經過兩次操作以後,變數的版本號就會由1變成3,也就是說只要執行緒對變數進行過操作,變數的版本號就會發生更改。從而解決了ABA問題。
解釋一下上邊的原始碼: 類中維護了一個volatile修飾的Pair型別變數current,Pair是一個私有的靜態類,current可以理解為底層數值。 compareAndSet方法的引數部分分別為期望的引用、新的引用、期望的版本號、新的版本號。 return的邏輯為判斷了期望的引用和版本號是否與底層的引用和版本號相符,並且排除了新的引用和新的版本號與底層的值相同的情況(即不需要修改)的情況(return程式碼部分3、4行)。條件成立,執行casPair方法,呼叫CAS操作。
AtomicLongArray
這個類實際上維護了一個Array陣列,我們在對數值進行更新的時候,會多一個索引值讓我們更新。
原子性,提供了互斥訪問,同一時刻只能有一個執行緒來對它進行操作。那麼在java裡,保證同一時刻只有一個執行緒對它進行操作的,除了Atomic包之外,還有鎖的機制。JDK提供鎖主要分為兩種:synchronized和Lock。接下來我們瞭解一下synchronized。
synchronized
依賴於JVM去實現鎖,因此在這個關鍵字作用物件的作用範圍內,都是同一時刻只能有一個執行緒對其進行操作的。 synchronized是java中的一個關鍵字,是一種同步鎖。它可以修飾的物件主要有四種:
修飾程式碼塊:大括號括起來的程式碼,作用於呼叫的物件
修飾方法:整個方法,作用於呼叫的物件
修飾靜態方法:整個靜態方法,作用於所有物件
修飾類:括號括起來的部分,作用於所有物件
具體的我就不寫了 網上太多了
Lock我需要整理 下次再說吧.
因為現在 synchronized現在加入了多種優化手段.其實效率來說 不低了.如果不能判斷用Lock和synchronized 那就用synchronized吧
複製一份總結
原子性操作各方法間的對比
- synchronized:不可中斷鎖,適合競爭不激烈,可讀性好 (其實我覺得現在效率可以了,可以直接用)
- Lock:可中斷鎖,多樣化同步,競爭激烈時能維持常態
- Atomic:競爭激烈時能維持常態,比Lock效能好,每次只能同步一個值