還在用Synchronized姊途?Atomic你了解不?

前言

只有光頭才能變強

之前已經(jīng)寫過多線程相關的文章了寞钥,有興趣的同學可以去了解一下:

多線程文章

在閱讀《阿里巴巴 Java開發(fā)手冊》讀后感時慌申,還有未解決的問題:

如果是count++操作,使用如下類實現(xiàn): AtomicInteger count = new AtomicInteger(); count.addAndGet(1);如果是 JDK8理郑,推薦使用 LongAdder 對象蹄溉,比 AtomicLong 性能更好(減少樂觀鎖的重試次數(shù))。

之前在學習的時候也看過AtomicInteger類很多次了您炉,一直沒有去做相關的筆記∑饩簦現(xiàn)在遇到問題了,于是就過來寫寫筆記赚爵,并希望在學習的過程中解決掉問題棉胀。

一、基礎鋪墊

首先我們來個例子:


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());
        }

        // 等待上述的線程執(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++;
    }
}

你們猜猜得出的結果是多少冀膝?是100嗎唁奢?

多運行幾次可以發(fā)現(xiàn):結果是不確定的,可能是95窝剖,也可能是98麻掸,也可能是100

結果不確定

根據(jù)結果我們得知:上面的代碼是線程不安全的!如果線程安全的代碼赐纱,多次執(zhí)行的結果是一致的脊奋!

我們可以發(fā)現(xiàn)問題所在:count++不是原子操作熬北。因為count++需要經(jīng)過讀取-修改-寫入三個步驟。舉個例子:

  • 如果某一個時刻:線程A讀到count的值是10诚隙,線程B讀到count的值也是10
  • 線程A對count++讶隐,此時count的值為11
  • 線程B對count++,此時count的值也是11(因為線程B讀到的count是10)
  • 所以到這里應該知道為啥我們的結果是不確定了吧久又。

要將上面的代碼變成線程安全的(每次得出的結果是100)巫延,那也很簡單,畢竟我們是學過synchronized鎖的人:

  • increase()加synchronized鎖就好了

public synchronized void increase() {
    count++;
}

無論執(zhí)行多少次地消,得出的都是100:

結果都是100

從上面的代碼我們也可以發(fā)現(xiàn)烈评,只做一個++這么簡單的操作,都用到了synchronized鎖犯建,未免有點小題大做了。

  • Synchronized鎖是獨占的瓜客,意味著如果有別的線程在執(zhí)行适瓦,當前線程只能是等待!

于是我們原子變量的類就登場了谱仪!

1.2CAS再來看看

在寫文章之前玻熙,本以為對CAS有一定的了解了(因為之前已經(jīng)看過相關概念,以為自己理解了)..但真正敲起鍵盤寫的時候疯攒,還是發(fā)現(xiàn)沒完全弄懂...所以再來看看CAS吧嗦随。

來源維基百科:

比較并交換(compare and swap, CAS),是原子操作的一種敬尺,可用于在多線程編程中實現(xiàn)不被打斷的數(shù)據(jù)交換操作枚尼,從而避免多線程同時改寫某一數(shù)據(jù)時由于執(zhí)行順序不確定性以及中斷的不可預知性產(chǎn)生的數(shù)據(jù)不一致問題。 該操作通過將內(nèi)存中的值與指定數(shù)據(jù)進行比較砂吞,當數(shù)值一樣時將內(nèi)存中的數(shù)據(jù)替換為新的值署恍。

CAS有3個操作數(shù):

  • 內(nèi)存值V
  • 舊的預期值A
  • 要修改的新值B

當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值(A和內(nèi)存值V相同時蜻直,將內(nèi)存值V修改為B)盯质,而其它線程都失敗,失敗的線程并不會被掛起概而,而是被告知這次競爭中失敗呼巷,并可以再次嘗試(或者什么都不做)

我們畫張圖來理解一下:

CAS理解

我們可以發(fā)現(xiàn)CAS有兩種情況:

  • 如果內(nèi)存值V和我們的預期值A相等赎瑰,則將內(nèi)存值修改為B王悍,操作成功!
  • 如果內(nèi)存值V和我們的預期值A不相等乡范,一般也有兩種情況:
    • 重試(自旋)
    • 什么都不做

我們再繼續(xù)往下看配名,如果內(nèi)存值V和我們的預期值A不相等時啤咽,應該什么時候重試,什么時候什么都不做渠脉。

1.2.1CAS失敗重試(自旋)

比如說宇整,我上面用了100個線程,對count值進行加1芋膘。我們都知道:如果在線程安全的情況下鳞青,這個count值最終的結果一定是為100的。那就意味著:每個線程都會對這個count值實質(zhì)地進行加1为朋。

我繼續(xù)畫張圖來說明一下CAS是如何重試(循環(huán)再試)的:

CAS循環(huán)重試

上面圖只模擬出兩個線程的情況臂拓,但足夠說明問題了。

1.2.2CAS失敗什么都不做

上面是每個線程都要為count值加1习寸,但我們也可以有這種情況:將count值設置為5

我也來畫個圖說明一下:

CAS失敗什么都不做

理解CAS的核心就是:CAS是原子性的胶惰,雖然你可能看到比較后再修改(compare and swap)覺得會有兩個操作,但終究是原子性的霞溪!

二孵滞、原子變量類簡單介紹

原子變量類在java.util.concurrent.atomic包下,總體來看有這么多個:

原子變量類

我們可以對其進行分類:

  • 基本類型:
    • AtomicBoolean:布爾型
    • AtomicInteger:整型
    • AtomicLong:長整型
  • 數(shù)組:
    • AtomicIntegerArray:數(shù)組里的整型
    • AtomicLongArray:數(shù)組里的長整型
    • AtomicReferenceArray:數(shù)組里的引用類型
  • 引用類型:
    • AtomicReference:引用類型
    • AtomicStampedReference:帶有版本號的引用類型
    • AtomicMarkableReference:帶有標記位的引用類型
  • 對象的屬性:
    • AtomicIntegerFieldUpdater:對象的屬性是整型
    • AtomicLongFieldUpdater:對象的屬性是長整型
    • AtomicReferenceFieldUpdater:對象的屬性是引用類型
  • JDK8新增DoubleAccumulator鸯匹、LongAccumulator坊饶、DoubleAdder、LongAdder
    • 是對AtomicLong等類的改進殴蓬。比如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í)行多少次须教,我們的結果永遠是100!

其實Atomic包下原子類的使用方式都不會差太多,了解原子類各種類型轻腺,看看API乐疆,基本就會用了(網(wǎng)上也寫得比較詳細,所以我這里果斷偷懶了)...

2.2ABA問題

使用CAS有個缺點就是ABA的問題贬养,什么是ABA問題呢挤土?首先我用文字描述一下:

  • 現(xiàn)在我有一個變量count=10,現(xiàn)在有三個線程误算,分別為A仰美、B、C
  • 線程A和線程C同時讀到count變量儿礼,所以線程A和線程C的內(nèi)存值和預期值都為10
  • 此時線程A使用CAS將count值修改成100
  • 修改完后咖杂,就在這時,線程B進來了蚊夫,讀取得到count的值為100(內(nèi)存值和預期值都是100)诉字,將count值修改成10
  • 線程C拿到執(zhí)行權,發(fā)現(xiàn)內(nèi)存值是10知纷,預期值也是10奏窑,將count值修改成11

上面的操作都可以正常執(zhí)行完的,這樣會發(fā)生什么問題呢屈扎??線程C無法得知線程A和線程B修改過的count值撩匕,這樣是有風險的鹰晨。

下面我再畫個圖來說明一下ABA的問題(以鏈表為例):

CAS 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 性能更好(減少樂觀鎖的重試次數(shù))则披。

去查閱了一些博客和資料,大概的意思就是:

  • 使用AtomicLong時洗出,在高并發(fā)下大量線程會同時去競爭更新同一個原子變量士复,但是由于同時只有一個線程的CAS會成功,所以其他線程會不斷嘗試自旋嘗試CAS操作翩活,這會浪費不少的CPU資源阱洪。
  • 而LongAdder可以概括成這樣:內(nèi)部核心數(shù)據(jù)value分離成一個數(shù)組(Cell)便贵,每個線程訪問時,通過哈希等算法映射到其中一個數(shù)字進行計數(shù),而最終的計數(shù)結果冗荸,則為這個數(shù)組的求和累加承璃。
    • 簡單來說就是將一個值分散成多個值,在并發(fā)的時候就可以分散壓力俏竞,性能有所提高绸硕。

參考資料:

最后

參考資料:

如果你覺得我寫得還不錯,了解一下:

  • 堅持原創(chuàng)的技術公眾號:Java3y魂毁〔E澹回復 1 加入Java交流群
  • 文章的目錄導航(精美腦圖+海量視頻資源):https://github.com/ZhongFuCheng3y/3y
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市席楚,隨后出現(xiàn)的幾起案子咬崔,更是在濱河造成了極大的恐慌,老刑警劉巖烦秩,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件垮斯,死亡現(xiàn)場離奇詭異,居然都是意外死亡只祠,警方通過查閱死者的電腦和手機兜蠕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抛寝,“玉大人熊杨,你說我怎么就攤上這事〉两ⅲ” “怎么了晶府?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長钻趋。 經(jīng)常有香客問我川陆,道長,這世上最難降的妖魔是什么蛮位? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任较沪,我火速辦了婚禮,結果婚禮上失仁,老公的妹妹穿的比我還像新娘购对。我一直安慰自己,他們只是感情好陶因,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布骡苞。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪解幽。 梳的紋絲不亂的頭發(fā)上贴见,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機與錄音躲株,去河邊找鬼片部。 笑死,一個胖子當著我的面吹牛霜定,可吹牛的內(nèi)容都是我干的档悠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼望浩,長吁一口氣:“原來是場噩夢啊……” “哼辖所!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起磨德,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤缘回,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后典挑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體酥宴,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年您觉,在試婚紗的時候發(fā)現(xiàn)自己被綠了拙寡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡琳水,死狀恐怖倒庵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情炫刷,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布郁妈,位于F島的核電站浑玛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏噩咪。R本人自食惡果不足惜顾彰,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望胃碾。 院中可真熱鬧涨享,春花似錦、人聲如沸仆百。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吁讨,卻和暖如春髓迎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背建丧。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工排龄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人翎朱。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓橄维,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拴曲。 傳聞我的和親對象是個殘疾皇子争舞,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容