你還在用Synchronized?Atomic你了解不?
前言
只有光頭才能變強
之前已經(jīng)寫過多線程相關(guān)的文章了,有興趣的同學(xué)可以去了解一下:
https://github.com/ZhongFuCheng3y/3y/blob/master/src/thread.md
在閱讀《阿里巴巴 Java開發(fā)手冊》讀后感時,還有未解決的問題:
如果是count++操作,使用如下類實現(xiàn): AtomicInteger count = new AtomicInteger(); count.addAndGet(1);如果是 JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數(shù))。
之前在學(xué)習(xí)的時候也看過AtomicInteger類很多次了,一直沒有去做相關(guān)的筆記?,F(xiàn)在遇到問題了,于是就過來寫寫筆記,并希望在學(xué)習(xí)的過程中解決掉問題。
一、基礎(chǔ)鋪墊
首先我們來個例子:
public class AtomicMain { public static void main(String[] args) throws InterruptedException { ExecutorService service = Executors.newCachedThreadPool(); Count count = new Count(); // 100個線程對共享變量進(jìn)行加1 for (int i = 0; i < 100; i++) { service.execute(() -> count.increase()); } // 等待上述的線程執(zhí)行完 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++; } }
你們猜猜得出的結(jié)果是多少?是100嗎?
多運行幾次可以發(fā)現(xiàn):結(jié)果是不確定的,可能是95,也可能是98,也可能是100
根據(jù)結(jié)果我們得知:上面的代碼是線程不安全的!如果線程安全的代碼,多次執(zhí)行的結(jié)果是一致的!
我們可以發(fā)現(xiàn)問題所在:count++
并不是原子操作。因為count++
需要經(jīng)過讀取-修改-寫入
三個步驟。舉個例子:
- 如果某一個時刻:線程A讀到count的值是10,線程B讀到count的值也是10
- 線程A對
count++
,此時count的值為11 - 線程B對
count++
,此時count的值也是11(因為線程B讀到的count是10) - 所以到這里應(yīng)該知道為啥我們的結(jié)果是不確定了吧。
要將上面的代碼變成線程安全的(每次得出的結(jié)果是100),那也很簡單,畢竟我們是學(xué)過synchronized鎖的人:
在increase()
加synchronized鎖就好了
public synchronized void increase() { count++; }
無論執(zhí)行多少次,得出的都是100:
從上面的代碼我們也可以發(fā)現(xiàn),只做一個++
這么簡單的操作,都用到了synchronized鎖,未免有點小題大做了。
Synchronized鎖是獨占的,意味著如果有別的線程在執(zhí)行,當(dāng)前線程只能是等待!
于是我們原子變量的類就登場了!
1.2CAS再來看看
在寫文章之前,本以為對CAS有一定的了解了(因為之前已經(jīng)看過相關(guān)概念,以為自己理解了)..但真正敲起鍵盤寫的時候,還是發(fā)現(xiàn)沒完全弄懂...所以再來看看CAS吧。
來源維基百科:
比較并交換(compare and swap, CAS),是原子操作的一種,可用于在多線程編程中實現(xiàn)不被打斷的數(shù)據(jù)交換操作,從而避免多線程同時改寫某一數(shù)據(jù)時由于執(zhí)行順序不確定性以及中斷的不可預(yù)知性產(chǎn)生的數(shù)據(jù)不一致問題。 該操作通過將內(nèi)存中的值與指定數(shù)據(jù)進(jìn)行比較,當(dāng)數(shù)值一樣時將內(nèi)存中的數(shù)據(jù)替換為新的值。
CAS有3個操作數(shù):
- 內(nèi)存值V
- 舊的預(yù)期值A(chǔ)
- 要修改的新值B
當(dāng)多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值(A和內(nèi)存值V相同時,將內(nèi)存值V修改為B),而其它線程都失敗,失敗的線程并不會被掛起,而是被告知這次競爭中失敗,并可以再次嘗試(或者什么都不做)。
我們畫張圖來理解一下:
我們可以發(fā)現(xiàn)CAS有兩種情況:
如果內(nèi)存值V和我們的預(yù)期值A(chǔ)相等,則將內(nèi)存值修改為B,操作成功!
如果內(nèi)存值V和我們的預(yù)期值A(chǔ)不相等,一般也有兩種情況:
- 重試(自旋)
- 什么都不做
我們再繼續(xù)往下看,如果內(nèi)存值V和我們的預(yù)期值A(chǔ)不相等時,應(yīng)該什么時候重試,什么時候什么都不做。
1.2.1CAS失敗重試(自旋)
比如說,我上面用了100個線程,對count值進(jìn)行加1。我們都知道:如果在線程安全的情況下,這個count值最終的結(jié)果一定是為100的。那就意味著:每個線程都會對這個count值實質(zhì)地進(jìn)行加1。
我繼續(xù)畫張圖來說明一下CAS是如何重試(循環(huán)再試)的:
上面圖只模擬出兩個線程的情況,但足夠說明問題了。
1.2.2CAS失敗什么都不做
上面是每個線程都要為count值加1,但我們也可以有這種情況:將count值設(shè)置為5
我也來畫個圖說明一下:
理解CAS的核心就是:CAS是原子性的,雖然你可能看到比較后再修改(compare and swap)覺得會有兩個操作,但終究是原子性的!
二、原子變量類簡單介紹
原子變量類在java.util.concurrent.atomic
包下,總體來看有這么多個:
我們可以對其進(jìn)行分類:
基本類型:
- AtomicBoolean:布爾型
- AtomicInteger:整型
- AtomicLong:長整型
數(shù)組:
- AtomicIntegerArray:數(shù)組里的整型
- AtomicLongArray:數(shù)組里的長整型
- AtomicReferenceArray:數(shù)組里的引用類型
引用類型:
- AtomicReference:引用類型
- AtomicStampedReference:帶有版本號的引用類型
- AtomicMarkableReference:帶有標(biāo)記位的引用類型
對象的屬性:
- AtomicIntegerFieldUpdater:對象的屬性是整型
- AtomicLongFieldUpdater:對象的屬性是長整型
- AtomicReferenceFieldUpdater:對象的屬性是引用類型
JDK8新增DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder
- 是對AtomicLong等類的改進(jìn)。比如LongAccumulator與LongAdder在高并發(fā)環(huán)境下比AtomicLong更高效。
Atomic包里的類基本都是使用Unsafe實現(xiàn)的包裝類。
Unsafe里邊有幾個我們喜歡的方法(CAS):
// 第一和第二個參數(shù)代表對象的實例以及地址,第三個參數(shù)代表期望值,第四個參數(shù)代表更新值 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包的類的實現(xiàn)絕大調(diào)用Unsafe的方法,而Unsafe底層實際上是調(diào)用C代碼,C代碼調(diào)用匯編,最后生成出一條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方法還是如上
修改完,無論執(zhí)行多少次,我們的結(jié)果永遠(yuǎn)是100!
其實Atomic包下原子類的使用方式都不會差太多,了解原子類各種類型,看看API,基本就會用了(網(wǎng)上也寫得比較詳細(xì),所以我這里果斷偷懶了)...
2.2ABA問題
使用CAS有個缺點就是ABA的問題,什么是ABA問題呢?首先我用文字描述一下:
- 現(xiàn)在我有一個變量
count=10
,現(xiàn)在有三個線程,分別為A、B、C - 線程A和線程C同時讀到count變量,所以線程A和線程C的內(nèi)存值和預(yù)期值都為10
- 此時線程A使用CAS將count值修改成100
- 修改完后,就在這時,線程B進(jìn)來了,讀取得到count的值為100(內(nèi)存值和預(yù)期值都是100),將count值修改成10
- 線程C拿到執(zhí)行權(quán),發(fā)現(xiàn)內(nèi)存值是10,預(yù)期值也是10,將count值修改成11
上面的操作都可以正常執(zhí)行完的,這樣會發(fā)生什么問題呢??線程C無法得知線程A和線程B修改過的count值,這樣是有風(fēng)險的。
下面我再畫個圖來說明一下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.
簡單來說就是在給為這個對象提供了一個版本,并且這個版本如果被修改了,是自動更新的。
原理大概就是:維護(hù)了一個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 性能更好(減少樂觀鎖的重試次數(shù))。
去查閱了一些博客和資料,大概的意思就是:
使用AtomicLong時,在高并發(fā)下大量線程會同時去競爭更新同一個原子變量,但是由于同時只有一個線程的CAS會成功,所以其他線程會不斷嘗試自旋嘗試CAS操作,這會浪費不少的CPU資源。
而LongAdder可以概括成這樣:內(nèi)部核心數(shù)據(jù)value分離成一個數(shù)組(Cell),每個線程訪問時,通過哈希等算法映射到其中一個數(shù)字進(jìn)行計數(shù),而最終的計數(shù)結(jié)果,則為這個數(shù)組的求和累加。
簡單來說就是將一個值分散成多個值,在并發(fā)的時候就可以分散壓力,性能有所提高。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 淺談Java中的atomic包實現(xiàn)原理及應(yīng)用
- Java多線程Atomic包操作原子變量與原子類詳解
- Java多線程atomic包介紹及使用方法
- Java AtomicInteger類的使用方法詳解
- Java concurrency之AtomicReference原子類_動力節(jié)點Java學(xué)院整理
- Java concurrency之AtomicLong原子類_動力節(jié)點Java學(xué)院整理
- Java concurrency之AtomicLongFieldUpdater原子類_動力節(jié)點Java學(xué)院整理
- Java concurrency之AtomicLongArray原子類_動力節(jié)點Java學(xué)院整理
- Java中對AtomicInteger和int值在多線程下遞增操作的測試
相關(guān)文章
構(gòu)建多模塊的Spring Boot項目步驟全紀(jì)錄
這篇文章主要給大家介紹了關(guān)于如何構(gòu)建多模塊的Spring Boot項目的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用SpringBoot具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05