Java 并發(fā)系列(一)多線程三大特性:原子性、可見性魁莉、有序性

概述

多線程三大特性:原子性睬涧、可見性、有序性旗唁。

1. 原子性

原子性是指:多個(gè)操作作為一個(gè)整體畦浓,不能被分割與中斷,也不能被其他線程干擾检疫。如果被中斷與干擾讶请,則會(huì)出現(xiàn)數(shù)據(jù)異常、邏輯異常屎媳。

多個(gè)操作合并的整體夺溢,我們稱之為復(fù)合操作。一個(gè)復(fù)合操作烛谊,往往存在前后依賴關(guān)系风响,后一個(gè)操作依賴上一個(gè)操作的結(jié)果。如果上一個(gè)操作結(jié)果被其他線程干擾丹禀,對(duì)于當(dāng)前線程看來整個(gè)復(fù)合操作的結(jié)果便不符合預(yù)期状勤。同理線程也不能在復(fù)合操作中間被中斷,中斷必須發(fā)生在進(jìn)入復(fù)合操作之前或者等到復(fù)合操作結(jié)束之后双泪。

保證原子性就是在多線程環(huán)境下持搜,保證單個(gè)線程執(zhí)行復(fù)合操作符合預(yù)期邏輯。

典型的復(fù)合操作:『先檢查后執(zhí)行』和『讀取—修改—寫入』

1.1 先檢查后執(zhí)行

@NotThreadSafe
public class LazyInitClass {
    private static LazyInitClass instance ;

    public static LazyInitClass getInstance() {
        if(instance == null)
            instance = new LazyInitClass() ;

        return instance ;
    }
}

LazyInitClassgetInstance 中包含先檢查后執(zhí)行的復(fù)合操作攒读,通常我們也可以稱 getInstance 中包含競(jìng)態(tài)條件朵诫。假設(shè)線程 A 和線程 B 同時(shí)執(zhí)行 getInstance辛友。A 看到 instance 為空薄扁,便執(zhí)行 new LazyInitClass() 邏輯。A 還未完成初始化并設(shè)置 instance废累,B 檢查 instance邓梅,此時(shí) instance 為空,B 便也會(huì)執(zhí)行 new LazyInitClass()邑滨。那么兩次調(diào)用 getInstance 時(shí)可能會(huì)得到不同的結(jié)果日缨。通常 getInstance 的預(yù)期結(jié)果是多次調(diào)用得到相同的對(duì)象實(shí)例。

LazyInitClassgetInstance 方法雖然存在競(jìng)態(tài)條件掖看,多數(shù)情況下并不會(huì)造成業(yè)務(wù)異常匣距,影響僅僅是增加了 JVM 垃圾回收負(fù)擔(dān)而已面哥。這也是多線程問題隱蔽性強(qiáng)且偶發(fā)的原因之一。

但話說回來毅待,編程原則之一就是所有邏輯都必須建立在確定性之上尚卫,任何建立在不確定性上的邏輯都是隱患。雖然從業(yè)務(wù)上看多數(shù)情況下沒問題尸红,但競(jìng)態(tài)條件的存在吱涉,讓代碼邏輯建立在不確定性之上。作為編碼者應(yīng)該重視此類問題外里。

1.2 讀取—修改—寫入

@NotThreadSafe
public class ReadModifyAndWriteClass {
    private int count = 0 ;

    public int increase() {
        return count++ ;
    }
}

由于 i++ 本身不是原子操作怎爵,屬于復(fù)合操作。ReadModifyAndWriteClassincrease 包含了讀取—修改—寫入盅蝗。假設(shè)線程 A 和線程 B 同時(shí)執(zhí)行 increase鳖链。A 看到 count 為 0,執(zhí)行 ++ 邏輯墩莫。當(dāng) ++ 操作還未完成撒轮,此時(shí) B 讀取 count 看到的仍然是 0。A贼穆、B 各自完成 ++ 邏輯后题山,count 的值等于 1。這就造成了雖然調(diào)用了兩次 increase 方法故痊,但 count 只增加了 1顶瞳。這也與預(yù)期:每調(diào)用一次 increase,count 增加 1 的結(jié)果不符愕秫。

2. 可見性

可見性問題是指慨菱,一個(gè)線程修改的共享變量,其他線程是否能夠立刻看到戴甩。對(duì)于串行程序而言符喝,并不存在可見性問題,前一個(gè)操作修改的變量甜孤,后一個(gè)操作一定能讀取到最新值协饲。但在多線程環(huán)境下如果沒有正確的同步則不一定。

有很多因素會(huì)使得線程無法立即看到甚至永遠(yuǎn)無法看到另一個(gè)線程的操作結(jié)果缴川。在編譯器中生成的指令順序茉稠,可以與源代碼中的順序不同,此外編譯器還會(huì)把變量保存在寄存器而非內(nèi)存中把夸;處理器可以采用亂序或并行等方式來執(zhí)行指令而线;緩存可能會(huì)改變將寫入變量提交到主內(nèi)存的次序;而且,保存在處理器本地緩存中的值膀篮,對(duì)于其他處理器是不可見的嘹狞。這些因素都會(huì)使得一個(gè)線程無法看到變量的最新值,并且會(huì)導(dǎo)致其他線程中的內(nèi)存操作似乎在亂序執(zhí)行誓竿。

2.1 緩存引起的可見性

multi-core processor.png

上圖是多核 CPU 內(nèi)存圖刁绒,其中 individual memory 表示核心多級(jí)緩存。main memory 表示主內(nèi)存烤黍,即共享內(nèi)存知市。共享內(nèi)存(shared memory)是線程之間共享的內(nèi)存,也稱為堆內(nèi)存(heap memory)速蕊。所有實(shí)例域(instance fields)嫂丙、靜態(tài)域(static fields)和數(shù)組元素(array elements)都保存在堆內(nèi)存中。

A 線程與 B 線程共同操作共享變量 V(初始值為 0)规哲,A跟啤、B 線程分別將 V 變量從主內(nèi)存復(fù)制到 CPU 內(nèi)核的多級(jí)緩存中,此時(shí) A 與 B 都讀到 V 的值為 0唉锌。A 更新自己的 individual memory 中的 V 的值為 1隅肥,此時(shí)如果沒有將 V 值同步至主內(nèi)存中,B 從自己的 individual memory 中讀到 V 的值仍然為 0袄简。當(dāng) V 值同步到主內(nèi)存后腥放,多級(jí)緩存失效,此時(shí) B 才能夠從主內(nèi)存中讀取到最新的 V 值為 1绿语。由于多線程環(huán)境下何時(shí)將多級(jí)緩存同步到主內(nèi)存時(shí)間上不確定秃症,所以造成了可見性問題,即 A 線程對(duì)共享變量 V 的寫操作吕粹,位于寫操作后執(zhí)行的 B 線程的讀操作不能立即感知种柑。

3. 有序性

有序性問題是指從觀察到的結(jié)果推測(cè),代碼執(zhí)行的順序與代碼組織的順序不一致匹耕。

3.1 指令重排序引起的有序性問題

在計(jì)算機(jī)體系結(jié)構(gòu)中聚请,為了提高執(zhí)行部件的處理速度,經(jīng)常在部件中采用流水線技術(shù)稳其。所謂流水線技術(shù)驶赏,是指將一個(gè)重復(fù)的時(shí)序過程,分解成若干個(gè)子過程欢际,而每一個(gè)子過程都可有效地在其專用功能段上與其他子過程同時(shí)執(zhí)行母市。

以 DLX 指令集結(jié)構(gòu)為例,一條指令的執(zhí)行簡(jiǎn)單說可以分為以下幾個(gè)步驟:

  1. 取指令(IF)
  2. 指令譯碼/讀寄存器(ID)
  3. 執(zhí)行/有效地址計(jì)算(EX)
  4. 存儲(chǔ)器訪問/分支完成(MEM)
  5. 寫回(WB)

每一個(gè)步驟都可能使用不同的硬件完成损趋。
指令流水線

由上圖所示,如果沒有指令流水線,指令2 需要等待指令1 完全執(zhí)行完成后執(zhí)行浑槽。假設(shè)每一個(gè)步驟(子過程)需要花費(fèi) 1 個(gè) CPU 時(shí)鐘周期蒋失,則指令2 需要等待 5 個(gè)時(shí)鐘周期。而使用指令流水線后篙挽,指令2 只需等待 1 個(gè)時(shí)鐘周期就可以開始執(zhí)行。指令2 開始執(zhí)行時(shí)镊靴,指令1 根本還沒開始執(zhí)行铣卡,僅僅完成了取指操作而已。這僅僅是 DLX 指令集結(jié)構(gòu)的流水線偏竟,實(shí)際商用 CPU 的流水線級(jí)別甚至可以達(dá)到 10 級(jí)以上煮落,性能提升可謂是非常明顯。

由于流水線技術(shù)的引入踊谋,不得不面對(duì)流水線的三種類型的相關(guān):結(jié)構(gòu)相關(guān)蝉仇、數(shù)據(jù)相關(guān)、控制相關(guān)殖蚕。

  1. 結(jié)構(gòu)相關(guān):當(dāng)指令在重疊執(zhí)行過程中轿衔,硬件資源滿足不了指令重疊執(zhí)行的要求,發(fā)生資源沖突時(shí)將產(chǎn)生“結(jié)構(gòu)相關(guān)”睦疫。
  2. 數(shù)據(jù)相關(guān):當(dāng)一條指令需要用到前面指令的執(zhí)行結(jié)果害驹,而這些指令均在流水線中重疊執(zhí)行時(shí),就可能引起“數(shù)據(jù)相關(guān)”蛤育。
  3. 控制相關(guān):當(dāng)流水線遇到分支指令和其他會(huì)改變 PC 值的指令時(shí)就會(huì)發(fā)生“控制相關(guān)”裙秋。

一旦流水線中出現(xiàn)相關(guān),指令在流失線中的執(zhí)行就會(huì)出現(xiàn)問題缨伊,消除相關(guān)的最基本方法是讓流水線中的某些指令暫停執(zhí)行摘刑。一旦暫停,所有硬件設(shè)備都會(huì)進(jìn)入一個(gè)停頓周期刻坊,直接影響是性能的下降枷恕。

我們說的指令重排序就是在產(chǎn)生數(shù)據(jù)相關(guān)時(shí)替代流水線暫停的重要方法。指令重排序僅僅是減少流水線暫停技術(shù)的一種谭胚,在 CPU 設(shè)計(jì)中還有很多其他軟硬件技術(shù)來防止流水線暫停徐块。

下圖展示了 A = B + C 操作的執(zhí)行過程。LW 表示加載灾而,LW R1, B 表示把 B 的值加載到寄存器 R1 中胡控。ADD 表示加法,ADD R3, R1, R2 表示把寄存器 R1 和 R2 中的值相加保存到寄存器 R3 中旁趟。SW 表示存儲(chǔ)昼激,SW A, R3 表示將寄存器 R3 中的值保存到變量 A 中。
A=B+C執(zhí)行流程

可以看到,ADD 指令的流水線上出現(xiàn)了一個(gè) stall橙困,表示一個(gè)暫停瞧掺。之所以出現(xiàn)暫停,是因?yàn)?R2 的數(shù)據(jù)還沒準(zhǔn)備好( LW R2, C 的操作還沒完成 )凡傅。由于 ADD 暫停的出現(xiàn)辟狈,后續(xù)的操作都暫停了一個(gè)周期。

下面是一個(gè)更為復(fù)雜的例子:
復(fù)雜計(jì)算執(zhí)行流程
可以看到夏跷,由于 ADD 和 SUB 指令都需要等待上一條指令的執(zhí)行結(jié)果哼转,所以整個(gè)流水線上插入了不少 stall。下圖顯示了如何消除類似的暫停槽华。
指令重排序以消除暫停
由于 LW Re, E; LW Rf, F 經(jīng)過指令重排序后壹蔓,并不影響代碼執(zhí)行邏輯。并且當(dāng)重排序后硼莽,所有流水線暫停都可以消除庶溶。
消除流水線暫停后

雖然指令重排序會(huì)導(dǎo)致有序性問題,但指令重排序?qū)π阅艿奶岣哂蟹浅V卮蟮囊饬x懂鸵。

3.2 CPU 緩存引起的有序性問題

2.1 節(jié)已經(jīng)討論過 CPU 緩存導(dǎo)致的可見性問題偏螺。CPU 緩存也會(huì)導(dǎo)致有序性問題。

看如下的例子:
CPU 緩存引起的有序性
假設(shè) b匆光、c 為局部變量套像,初始值為 1,A终息、D 為共享變量夺巩,初始值為 0 和 false。Thread1 先于 Thread2 運(yùn)行周崭,運(yùn)行結(jié)果:Thread2 輸出 0柳譬。

從結(jié)果推測(cè) Thread1 中的 D = true 先于 A = b + c 執(zhí)行了。

當(dāng) D = true 執(zhí)行完成后续镇,A = b + c 還沒來得及執(zhí)行美澳,此時(shí) Thread2 輸出 A 的值,才會(huì)出現(xiàn)結(jié)果為 0 的情況摸航。

分析:Thread1 將 A制跟、D 共享變量從主內(nèi)存復(fù)制到當(dāng)前 CPU 內(nèi)核的多級(jí)緩存中,按順序執(zhí)行完 A = b + c 和 D = true 后酱虎,多級(jí)緩存中 A = 2, D = true雨膨。然后 Thread1 將 D 的值優(yōu)先同步到主緩存,A 的值沒有同步到主緩存读串。此時(shí) Thread2 執(zhí)行聊记,能看到 D 的最新值 true撒妈,卻不能看到 A 的最新值,只能看到主緩存中 A 的初始值 0甥雕。

所以從 Thread2 看踩身,Thread1 線程的執(zhí)行出現(xiàn)了有序性問題胀茵,但從 Thread1 看社露,自己的確是按照代碼組織順序執(zhí)行的。

4. 總結(jié)

本章詳細(xì)講解了多線程的三大特性:原子性琼娘、可見性峭弟、有序性。想要正確編寫多線程程序脱拼,一定要正確理解這三大特性瞒瘸。

5. 參考資料

  1. 《The Java? LanguageSpecification Java SE 8 Edition》作者:James Gosling、Bill Joy熄浓、Guy Steele情臭、Gilad Bracha、Alex Buckley
  2. 《Java Concurrency in Practice》作者:Brain Goetz赌蔑、Tim Peierls俯在、Joshua Bloch、Joseph Bowbeer娃惯、David Holmes跷乐、Doug Lea
  3. 《計(jì)算機(jī)體系結(jié)構(gòu)》作者:張晨曦、王志英趾浅、張春元愕提、戴葵、朱海濱
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末皿哨,一起剝皮案震驚了整個(gè)濱河市浅侨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌证膨,老刑警劉巖如输,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異椎例,居然都是意外死亡挨决,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門订歪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脖祈,“玉大人,你說我怎么就攤上這事刷晋「歉撸” “怎么了慎陵?”我有些...
    開封第一講書人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長喻奥。 經(jīng)常有香客問我席纽,道長,這世上最難降的妖魔是什么撞蚕? 我笑而不...
    開封第一講書人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任润梯,我火速辦了婚禮,結(jié)果婚禮上甥厦,老公的妹妹穿的比我還像新娘纺铭。我一直安慰自己,他們只是感情好刀疙,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開白布舶赔。 她就那樣靜靜地躺著,像睡著了一般谦秧。 火紅的嫁衣襯著肌膚如雪竟纳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評(píng)論 1 285
  • 那天疚鲤,我揣著相機(jī)與錄音锥累,去河邊找鬼。 笑死石咬,一個(gè)胖子當(dāng)著我的面吹牛揩悄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鬼悠,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼删性,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了焕窝?” 一聲冷哼從身側(cè)響起蹬挺,我...
    開封第一講書人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎它掂,沒想到半個(gè)月后巴帮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡虐秋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年榕茧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片客给。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡用押,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出靶剑,到底是詐尸還是另有隱情蜻拨,我是刑警寧澤池充,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站缎讼,受9級(jí)特大地震影響收夸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜血崭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一卧惜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧功氨,春花似錦序苏、人聲如沸手幢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽围来。三九已至跺涤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間监透,已是汗流浹背桶错。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留胀蛮,地道東北人院刁。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像粪狼,于是被迫代替她去往敵國和親退腥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

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