Java高并發(fā)--原子性可見性有序性

Java高并發(fā)--原子性可見性有序性

主要是學(xué)習(xí)慕課網(wǎng)實(shí)戰(zhàn)視頻《Java并發(fā)編程入門與高并發(fā)面試》的筆記

  • 原子性:指一個(gè)操作不可中斷驶乾,一個(gè)線程一旦開始邑飒,直到執(zhí)行完成都不會(huì)被其他線程干擾。換句話說原子性保證了任何時(shí)刻只有一個(gè)線程在對共享變量進(jìn)行操作级乐。
  • 可見性:指當(dāng)一個(gè)線程修改了某個(gè)共享變量的值疙咸,其他線程是否能立即知道這個(gè)修改。
  • 有序性:一個(gè)線程觀察其他線程中的指令风科,由于指令重排序的存在撒轮,該觀察結(jié)果一般雜亂無序

原子性

AtomicInteger

JDK的atomic包下提供了許多“原子類”,它們都是基于CAS操作實(shí)現(xiàn)的贼穆。

所謂CAS(Compare And Swap)题山,即“比較并交換”。CAS基于樂觀的態(tài)度扮惦,是無鎖操作臀蛛,它操作包含三個(gè)參數(shù),當(dāng)前要更新的變量崖蜜、期望值浊仆、新值,僅當(dāng):當(dāng)前值和預(yù)期值一樣時(shí)豫领,才會(huì)將當(dāng)前值設(shè)置為新值抡柿;如果當(dāng)前值和預(yù)期值不一樣,說明這個(gè)變量已經(jīng)被其他線程修改過了等恐。如果有多個(gè)線程同時(shí)使用CAS操作一個(gè)變量時(shí)洲劣,只有一個(gè)會(huì)勝出备蚓,并成功更新。

以atomic包中最常用的AtomicInteger為例囱稽,追蹤其getAndIncrement()郊尝,

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

可以看到它調(diào)用了unsafe.getAndAddInt(this, valueOffset, 1)

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

其中var1表示要被更新的對象,var2是原始值在內(nèi)存中的偏移地址战惊。通過getIntVolatile(var1, var2);拿到現(xiàn)在的值var5流昏。但多個(gè)線程修改下,內(nèi)存中的原始值隨時(shí)都可能變化吞获,所以現(xiàn)在var5是一個(gè)期望值(期望內(nèi)存中的值和剛讀取到的var5是相等的况凉,因?yàn)閮?nèi)存中的值此時(shí)可能已經(jīng)變了)。compareAndSwapInt是一個(gè)native方法各拷,compareAndSwapInt(var1, var2, var5, var5 + var4)這句的意思是對于var1對象刁绒,根據(jù)偏移地址var2拿到的內(nèi)存中的原始值,如果和期望值var5相等烤黍,則將其更新為var5 + var4知市。同時(shí)從while-do也可以直到,該方法在一直嘗試速蕊,直到內(nèi)存中的值和期望值一樣時(shí)初狰,才能進(jìn)行修改,并返回修改前內(nèi)存中的值互例。

這里只是舉例解釋了其中一個(gè)方法奢入,其他方法的實(shí)現(xiàn)大同小異,總之都是使用了CAS操作來保證線程安全媳叨。

類似的還有AtomicLong腥光,AtomicBoolean,值得一提的還有有一個(gè)compareAndSet方法糊秆,當(dāng)且僅當(dāng)期望值except和內(nèi)存中的值相等時(shí)武福,才會(huì)執(zhí)行更新操作。可以保證在多線程下同時(shí)修改共享變量痘番,只有一個(gè)線程可以修改成功捉片。

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

舉個(gè)例子

package com.shy.concurrency.count;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author Haiyu
 * @date 2018/12/17 17:33
 */
public class Test {
    public static void main(String[] args) {
        AtomicInteger a = new AtomicInteger(9);
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0;i < 10; i++) {
            executorService.execute(()-> {
                if (a.compareAndSet(9, 10)) {
                    System.out.println("更新成功");
                    System.out.println(a.get());
                }
            });
        }
        executorService.shutdown();
    }


}

上面有十個(gè)線程要修改a的值,但是最后程序只打印了一次10汞舱。因?yàn)橐坏┠硞€(gè)程序成功將a更新成10伍纫,其他線程的期望值except就和現(xiàn)在內(nèi)存中的值10不相等了,所以都會(huì)更新失敗昂芜。

LongAdder

像AtomicInteger等原子類使用CAS操作雖然沒有鎖莹规,但是也可以使用減小鎖粒度這種分離熱點(diǎn)的思想。LongAdder正是這樣的類泌神,它也位于atomic包下良漱,且有著比AtomicInteger等原子類更好的性能舞虱。LongAdder有一個(gè)稱為base的變量,如果在多線程下對base的修改沒有發(fā)生沖突的話母市,會(huì)直接操作base變量矾兜;但是如果發(fā)生了沖突,base這個(gè)熱點(diǎn)數(shù)據(jù)會(huì)被分離成多個(gè)單元cell患久,每個(gè)單元獨(dú)立維護(hù)內(nèi)部的值(通過哈希算法定位到數(shù)組中的某個(gè)cell)焕刮。這個(gè)對象的值其實(shí)是由cell數(shù)組的求和累加得到的,這樣熱點(diǎn)就進(jìn)行了有效的分離墙杯,提高了并行度。

AtomicReference

AtomicReference和AtomicInteger十分相似括荡,不過一個(gè)是對整數(shù)的封裝高镐,一個(gè)是對普通對象的封裝。

AtomicReference<Integer> money = new AtomicReference<>(); 

這樣寫就行了畸冲,泛型類型是Integer嫉髓,因此可以實(shí)現(xiàn)和AtomicInteger相同的功能。

AtomicStampedReference

CAS可能引發(fā)"ABA"問題邑闲,即一個(gè)變量原來是A算行,先被修改成B后又修改回了A,由于CAS操作只是比較當(dāng)前值和預(yù)期值是否一樣(只比較結(jié)果苫耸,不在乎過程中狀態(tài)的變化)州邢,在其他線程來看,該變量就好像沒有發(fā)生過變化褪子。

可以為數(shù)據(jù)添加時(shí)間戳量淌,每次成功修改數(shù)據(jù)時(shí),不僅更新數(shù)據(jù)的值嫌褪,同時(shí)要更新時(shí)間戳的值呀枢。CAS操作時(shí),不僅要比較當(dāng)前值和預(yù)期值笼痛,還要比較當(dāng)前時(shí)間戳和預(yù)期時(shí)間戳裙秋。兩者都必須滿足預(yù)期值才能修改成功。

AtomicStampedReference正是這樣做的缨伊,它不僅維護(hù)對象值摘刑,還維護(hù)了一個(gè)時(shí)間戳(其實(shí)可就是一個(gè)版本號)。當(dāng)AtomicStampedReference對應(yīng)的值被修改時(shí)刻坊,不僅要更新數(shù)據(jù)本身泣侮,還要更新時(shí)間戳(版本號)。只有當(dāng)數(shù)據(jù)本身和時(shí)間戳都滿足期望值紧唱,寫入才會(huì)成功活尊。因此隶校,雖然對象值被反復(fù)修改又被更新成了原來的值,但是時(shí)間戳發(fā)生了變化蛹锰,就可以防止不恰當(dāng)?shù)膶懭搿?/p>

AtomicIntegerFieldUpdater

該類可以使普通變量也擁有原子操作深胳。首先保證該普通變量是volatile的(且不能有static修飾符)。然后像下面這樣使用铜犬。

AtomicIntegerFieldUpdater<Money> updater = AtomicIntegerFieldUpdater.newUpdater(Money.class, "money");

其中Money類中有一個(gè)“money”的字段舞终,它的類型是普通的int型。通過上面的用法癣猾,使得Money中的money字段也擁有的原子性敛劝。

class Money {
    volatile int money;

    public int getMoney() {
        return money;
    }
}

運(yùn)行以下程序,將總得到打印值為10纷宇,說明這是線程安全的

package com.shy.concurrency.count;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author Haiyu
 * @date 2018/12/17 17:33
 */
@ThreadSafe
public class Test {
    private volatile int count;

    public int getCount() {
        return count;
    }

    static AtomicIntegerFieldUpdater<Money> updater = AtomicIntegerFieldUpdater.newUpdater(Money.class, "money");

    public static void main(String[] args) throws InterruptedException {
        final Money money = new Money();
        CountDownLatch cdl = new CountDownLatch(10);

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                updater.incrementAndGet(money);
                cdl.countDown();
            });
        }
        
        cdl.await();
        System.out.println(money.getMoney());
        executorService.shutdown();
    }
}

AtomicIntegerArray

除了普通對象夸盟、基本數(shù)據(jù)類型的包裝類,atomic包還提供了原子數(shù)組像捶。如AtomicIntegerArray上陕,AtomicLongArray,在使用上無非就是加上了索引拓春。

比如

public final boolean compareAndSet(int i, int expect, int update) {...}

i就是數(shù)組的索引释簿,其他API也類似,需要指定一個(gè)索引來明確表明要對哪一個(gè)變量進(jìn)行原子操作硼莽。

Java中有synchronized和重入鎖來保證線程的同步庶溶,以實(shí)現(xiàn)線程安全。

synchronized是JVM的內(nèi)置鎖懂鸵,而重入鎖是Java代碼實(shí)現(xiàn)的渐尿。重入鎖是synchronized的擴(kuò)展,可以完全代替后者矾瑰。重入鎖可以重入砖茸,允許同一個(gè)線程連續(xù)多次獲得同一把鎖。其次殴穴,重入鎖獨(dú)有的功能有:

  • 可以相應(yīng)中斷凉夯,synchronized要么獲得鎖執(zhí)行,要么保持等待采幌。而重入鎖可以響應(yīng)中斷劲够,使得線程在遲遲得不到鎖的情況下,可以不再等待休傍。主要由lockInterruptibly()實(shí)現(xiàn)征绎,這是一個(gè)可以對中斷進(jìn)行響應(yīng)的鎖申請動(dòng)作,鎖中斷可以避免死鎖。
  • 鎖的申請可以有等待時(shí)限人柿,用tryLock()可以實(shí)現(xiàn)限時(shí)等待柴墩,如果超時(shí)還未獲得鎖會(huì)返回false,也防止了線程遲遲得不到鎖時(shí)一直等待凫岖,可避免死鎖江咳。
  • 公平鎖,即鎖的獲得按照線程先來后到的順序依次獲得哥放,不會(huì)產(chǎn)生饑餓現(xiàn)象歼指。synchronized的鎖默認(rèn)是不公平的,重入鎖可通過傳入構(gòu)造方法的參數(shù)實(shí)現(xiàn)公平鎖甥雕。
  • 重入鎖可以綁定多個(gè)Condition條件踩身,這些condition通過調(diào)用await/singal實(shí)現(xiàn)線程間通信。

synchronized可以作用在如下四個(gè)地方:

  • 代碼塊社露,使用當(dāng)前對象或其他任意對象作為鎖挟阻。被代碼塊包圍的代碼會(huì)同步執(zhí)行。synchronized(this)synchronized(obj)就分別使用自身和obj對象作為鎖呵哨。
  • 修飾方法,使用當(dāng)前對象作為鎖轨奄。整個(gè)方法會(huì)同步執(zhí)行
  • 修飾靜態(tài)方法孟害,使用類作為鎖(因此作用于該類的所有對象)。整個(gè)方法會(huì)同步執(zhí)行

注意在多線程下如果要保證synchronized的線程安全挪拟,必須使用同一把鎖挨务。

可見性

導(dǎo)致可見性的原因:

  • 線程交叉執(zhí)行
  • 指令重排結(jié)合線程交叉執(zhí)行
  • 共享變量更新后沒有在工作內(nèi)存和主內(nèi)存之間及時(shí)更新

synchronized的可見性

  • 線程解鎖前,必須將共享變量的最新值刷回主內(nèi)存中玉组。
  • 線程加鎖時(shí)谎柄,將清空工作內(nèi)存中共享變量的值,從而使用共享變量時(shí)需要從主內(nèi)存中重新讀取最新的值(注意加鎖與解鎖使用同一個(gè)鎖)惯雳。

volatile的可見性

  • 對volatile變量的寫操作朝巫,會(huì)在寫操作后加入一條store屏障指令,將工作內(nèi)存中的共享變量值刷新到主內(nèi)存中石景;
  • 對volatile變量的讀操作劈猿,會(huì)在讀操作前加入一條load屏障指令,讀主內(nèi)存中讀取共享變量

換句話說潮孽,volatile的作用是:在本CPU對變量的修改直接寫入主內(nèi)存中揪荣,同時(shí)這個(gè)寫操作使得其他CPU中對應(yīng)變量的緩存行無效,這樣其他線程在讀取這個(gè)變量時(shí)候必須從主內(nèi)存中讀取往史,所以讀取到的是最新的仗颈,這就是上面說得能被立即“看到”。

image
image

volatile常使用于標(biāo)志位的判斷椎例。

volatile boolean ready= false;
// 線程1
config = loadConfig(); // 語句1
ready = true;  // 語句2

// 線程2,ready為true時(shí)才停止sleep()
while (!ready) {
    sleep();
}
runWithConfig(config);

如上面的例子挨决,如果ready變量不是volatile的请祖,有可能因?yàn)橹噶钪嘏牛葓?zhí)行語句2再執(zhí)行語句1凰棉。由于先執(zhí)行語句2损拢,那么線程2中再config還沒有初始化時(shí)就執(zhí)行了runWithConfig(config),這顯然是不合理的撒犀。當(dāng)ready加上了volatile修飾符福压,禁止了指令重排,因此不會(huì)發(fā)生以上情況或舞。

有序性

Happen-Before規(guī)則

有些指令是可以重排的荆姆,有些指令是不可重排的。下面是一些基本原則:

  • 程序順序原則:一個(gè)線程內(nèi)保證語義的串行性映凳,比如第二條語句依賴第一條語句的結(jié)果胆筒,那么就不能先執(zhí)行第二條再執(zhí)行第一條。
  • volatile原則:volatile變量的寫先于讀诈豌,著保證了volatile變量的可見性
  • 鎖規(guī)則:先解鎖仆救,后續(xù)步驟再加鎖。加鎖不能重排到解鎖之前矫渔,這樣加鎖行為無法獲得鎖(剛加上就解了)
  • 傳遞性:A先于B彤蔽,B先于C,那么A先于C
  • 線程的start()先于它的每個(gè)動(dòng)作
  • 線程的所有操作先于線程的終結(jié)(可以通過Tread.join()方法結(jié)束庙洼、Thread.isAlive()的返回值判斷一個(gè)線程是否終結(jié))
  • 線程的中斷(interrupt())先于被中斷線程的代碼
  • 對象的構(gòu)造函數(shù)執(zhí)行顿痪、結(jié)束先于finalize()方法。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末油够,一起剝皮案震驚了整個(gè)濱河市蚁袭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌石咬,老刑警劉巖揩悄,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鬼悠,居然都是意外死亡虏束,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進(jìn)店門厦章,熙熙樓的掌柜王于貴愁眉苦臉地迎上來临梗,“玉大人屯吊,你說我怎么就攤上這事。” “怎么了彩匕?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長发乔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任雪猪,我火速辦了婚禮栏尚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘只恨。我一直安慰自己译仗,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布官觅。 她就那樣靜靜地躺著纵菌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪休涤。 梳的紋絲不亂的頭發(fā)上咱圆,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天,我揣著相機(jī)與錄音功氨,去河邊找鬼序苏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛捷凄,可吹牛的內(nèi)容都是我干的忱详。 我是一名探鬼主播,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼纵势,長吁一口氣:“原來是場噩夢啊……” “哼踱阿!你這毒婦竟也來了管钳?” 一聲冷哼從身側(cè)響起钦铁,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎才漆,沒想到半個(gè)月后牛曹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡醇滥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年黎比,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸳玩。...
    茶點(diǎn)故事閱讀 39,754評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡阅虫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出不跟,到底是詐尸還是另有隱情颓帝,我是刑警寧澤,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站购城,受9級特大地震影響吕座,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瘪板,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一吴趴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧侮攀,春花似錦锣枝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至箭昵,卻和暖如春税朴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背家制。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工正林, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人颤殴。 一個(gè)月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓觅廓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親涵但。 傳聞我的和親對象是個(gè)殘疾皇子杈绸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,654評論 2 354

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