前言
隻有光頭才能變強
之前已經寫過多線程相關的文章了,有興趣的同學可以去了解一下:
在閱讀
《阿裡巴巴 Java開發手冊》讀後感時,還有未解決的問題:
如果是count++操作,使用如下類實作: AtomicInteger count = new AtomicInteger(); count.addAndGet(1);如果是 JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數)。
之前在學習的時候也看過AtomicInteger類很多次了,一直沒有去做相關的筆記。現在遇到問題了,于是就過來寫寫筆記,并希望在學習的過程中解決掉問題。
一、基礎鋪墊
首先我們來個例子:
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的值為11count++
- 線程B對
,此時count的值也是11(因為線程B讀到的count是10)count++
- 是以到這裡應該知道為啥我們的結果是不确定了吧。
要将上面的代碼變成線程安全的(每次得出的結果是100),那也很簡單,畢竟我們是學過synchronized鎖的人:
- 在
加synchronized鎖就好了increase()
public synchronized void increase() {
count++;
}
無論執行多少次,得出的都是100:
從上面的代碼我們也可以發現,隻做一個
++
這麼簡單的操作,都用到了synchronized鎖,未免有點小題大做了。
- Synchronized鎖是獨占的,意味着如果有别的線程在執行,目前線程隻能是等待!
于是我們原子變量的類就登場了!
1.2CAS再來看看
在寫文章之前,本以為對CAS有一定的了解了(因為之前已經看過相關概念,以為自己了解了)..但真正敲起鍵盤寫的時候,還是發現沒完全弄懂...是以再來看看CAS吧。
來源維基百科:
比較并交換(compare and swap, CAS),是原子操作的一種,可用于在多線程程式設計中實作不被打斷的資料交換操作,進而避免多線程同時改寫某一資料時由于執行順序不确定性以及中斷的不可預知性産生的資料不一緻問題。 該操作通過将記憶體中的值與指定資料進行比較,當數值一樣時将記憶體中的資料替換為新的值。
CAS有3個操作數:
- 記憶體值V
- 舊的預期值A
- 要修改的新值B
當多個線程嘗試使用CAS同時更新同一個變量時,隻有其中一個線程能更新變量的值(A和記憶體值V相同時,将記憶體值V修改為B),而其它線程都失敗,失敗的線程并不會被挂起,而是被告知這次競争中失敗,并可以再次嘗試(或者什麼都不做)。
我們畫張圖來了解一下:
我們可以發現CAS有兩種情況:
- 如果記憶體值V和我們的預期值A相等,則将記憶體值修改為B,操作成功!
- 如果記憶體值V和我們的預期值A不相等,一般也有兩種情況:
- 重試(自旋)
- 什麼都不做
我們再繼續往下看,如果記憶體值V和我們的預期值A不相等時,應該什麼時候重試,什麼時候什麼都不做。
1.2.1CAS失敗重試(自旋)
比如說,我上面用了100個線程,對count值進行加1。我們都知道:如果線上程安全的情況下,這個count值最終的結果一定是為100的。那就意味着:每個線程都會對這個count值實質地進行加1。
我繼續畫張圖來說明一下CAS是如何重試(循環再試)的:
上面圖隻模拟出兩個線程的情況,但足夠說明問題了。
1.2.2CAS失敗什麼都不做
上面是每個線程都要為count值加1,但我們也可以有這種情況:将count值設定為5
我也來畫個圖說明一下:
了解CAS的核心就是:CAS是原子性的,雖然你可能看到比較後再修改(compare and swap)覺得會有兩個操作,但終究是原子性的!
二、原子變量類簡單介紹
原子變量類在
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問題呢?首先我用文字描述一下:
- 現在我有一個變量
,現在有三個線程,分别為A、B、Ccount=10
- 線程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的問題(以連結清單為例):
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),每個線程通路時,通過哈希等算法映射到其中一個數字進行計數,而最終的計數結果,則為這個數組的求和累加。
- 簡單來說就是将一個值分散成多個值,在并發的時候就可以分散壓力,性能有所提高。
參考資料:
- AtomicLong與LongAdder性能對比 https://zhuanlan.zhihu.com/p/45489739
- LongAdder源碼詳解 https://zhuanlan.zhihu.com/p/38288416
最後
- Java并發程式設計劄記-目錄 https://blog.csdn.net/panweiwei1994/article/details/78483167
- 《Java并發程式設計實戰》
- 《Java并發程式設計的藝術》
如果你覺得我寫得還不錯,了解一下:
- 文章的目錄導航(精美腦圖+海量視訊資源): https://github.com/ZhongFuCheng3y/3y