java并發(fā)與多線程(四):線程同步

1 同步是什么

資源共享的兩個(gè)原因是資源緊缺和共建需求莺掠。線程共享CPU是從資源緊缺的維度來(lái)考慮的衫嵌,而多線程共享同一變量,通常是從共建需求的維度來(lái)考慮的彻秆。在多個(gè)線程對(duì)同一變量進(jìn)行寫操作時(shí)楔绞,如果操作沒(méi)有原子性,就可能產(chǎn)生臟數(shù)據(jù)唇兑。所謂原子性是指不可分割的一系列操作指令酒朵,在執(zhí)行完畢前不會(huì)被任何其他操作中斷,要么全部執(zhí)行扎附,要么全部不執(zhí)行蔫耽。如果每個(gè)線程的修改都是原子操作,就不存在線程同步問(wèn)題帕棉。有些看似非常簡(jiǎn)單的操作其實(shí)不具備原子性针肥,典型的就是i++操作,它需要分為三步香伴,即ILOAD->IINC->ISTORE慰枕。另一方面,更加復(fù)雜的CAS(Compare And)操作卻具有原子性即纲。

計(jì)算機(jī)的形成同步具帮,就是線程之間按某種機(jī)制協(xié)調(diào)先后次序執(zhí)行,當(dāng)有一個(gè)線程在對(duì)內(nèi)存進(jìn)行操作時(shí)低斋,其他線程都不可以對(duì)這個(gè)內(nèi)存地址進(jìn)行操作蜂厅,直到該線程完成操作。實(shí)現(xiàn)線程同步的方式有很多膊畴,比如同步方法掘猿、鎖、阻塞隊(duì)列等唇跨。

2 volatile

先從happen before了解線程操作的可見(jiàn)性稠通。把happen before定義為方法hb(a,b)衬衬,表示a happen before b。如果hb(a,b)且hb(b,c)改橘,能夠推導(dǎo)出hb(a,c)滋尉。類似于x>y且y>z,可以推導(dǎo)出x>z飞主。這不就是一種放之四海而皆準(zhǔn)的規(guī)律嗎狮惜?但其實(shí)很多場(chǎng)景并不符合這種規(guī)律,比如在2018年俄羅斯世界杯上碌识,韓國(guó)隊(duì)?wèi)?zhàn)勝德國(guó)隊(duì)碾篡,德國(guó)隊(duì)?wèi)?zhàn)勝瑞典隊(duì),并不能推導(dǎo)出韓國(guó)隊(duì)?wèi)?zhàn)勝了瑞典隊(duì)丸冕。

線程執(zhí)行或線程切換都是納秒級(jí)的耽梅,執(zhí)行速度如此之快,直覺(jué)上會(huì)認(rèn)為線程本地緩存的必要性特別弱胖烛。做個(gè)類比眼姐,我們?nèi)祟愐阅隇橛?jì)而宇宙以億年為計(jì),宇宙老人看待人類的心態(tài)不正如我們看待CPU世界的心態(tài)嗎佩番?時(shí)間成本的巨大差異只要存在众旗,緩存策略自然就會(huì)產(chǎn)生。再比如趟畏,去學(xué)校圖書(shū)館僅需要10分鐘贡歧,借一本書(shū),無(wú)需緩存赋秀。但是如果去市圖書(shū)館利朵,往返需要5個(gè)小時(shí),一般為了減少路程開(kāi)銷而會(huì)考慮多借幾本猎莲。CPU訪問(wèn)內(nèi)存遠(yuǎn)遠(yuǎn)比訪問(wèn)告訴緩存L1和L2慢得多绍弟,對(duì)應(yīng)借書(shū)的例子,應(yīng)該得去國(guó)外圖書(shū)館了著洼。

接著再談指令優(yōu)化樟遣。計(jì)算機(jī)并不會(huì)根據(jù)代碼順序按部就班地執(zhí)行相關(guān)指令,再回到借書(shū)的例子身笤,加入你剛好要去還書(shū)豹悬,然后再借一本,你的室友恰好也讓你幫他歸還Easy Coding這本書(shū)液荸,然后再借一本《碼出高效》瞻佛。這個(gè)過(guò)程有兩件事,你的事和他的事娇钱。先辦你的事涤久,再辦他的事涡尘,是一種單線程的死板行為忍弛。此時(shí)你會(huì)潛意識(shí)地進(jìn)行“指令優(yōu)化”:把你要還的數(shù)和Easy Coding先一起歸還响迂,再一起借你們要借的書(shū),這就相當(dāng)于合并數(shù)據(jù)進(jìn)行存取的操作過(guò)程细疚。CPU在處理信息時(shí)也會(huì)進(jìn)行指令優(yōu)化蔗彤,分析哪些取數(shù)據(jù)動(dòng)作可以合并進(jìn)行,哪些存數(shù)據(jù)動(dòng)作可以合并進(jìn)行疯兼。CPU拜訪一趟遙遠(yuǎn)的內(nèi)存然遏,一定會(huì)到處看看,是否可以存取合并吧彪,以提高執(zhí)行效率待侵。指令重排示例代碼如下:

image.png

happen before是時(shí)鐘順序的先后,并不能保證線程交互的可見(jiàn)性姨裸。在第2處和第3處都是寫操作秧倾,不會(huì)進(jìn)行指令重排,但是前三行是不互斥的傀缩,并且第1處的操作如果放在z=3賦值操作之后那先,明顯是效率最大化的處理方式。所以指令重排的最大可能是把第1處和第2處串聯(lián)一次執(zhí)行赡艰。Happen before并不能保證線程交互的可見(jiàn)性售淡。那么什么是可見(jiàn)性呢?可見(jiàn)性是指某線程修改共享變量的指令對(duì)其他線程來(lái)說(shuō)都是可見(jiàn)的慷垮,它反映的是指令執(zhí)行的實(shí)時(shí)透明度揖闸。

每個(gè)線程都有獨(dú)占的內(nèi)存區(qū)域,如操作棧料身、本地變量表等汤纸。線程本地內(nèi)存保存了引用變量在堆內(nèi)存中的副本,線程對(duì)變量的所有操作都在本地內(nèi)存區(qū)域中進(jìn)行惯驼,執(zhí)行結(jié)束后再同步到堆內(nèi)存中去蹲嚣。這里必然有一個(gè)時(shí)間差,在這個(gè)時(shí)間差內(nèi)祟牲,改線程對(duì)副本的操作隙畜,對(duì)于其他線程是不可見(jiàn)的。

Volatile的英文本義是“揮發(fā)说贝、不穩(wěn)定的”议惰,延伸意義為敏感的。當(dāng)使用volatile修飾變量時(shí)乡恕,意味著任何對(duì)此變量的操作都會(huì)在內(nèi)存中進(jìn)行言询,不會(huì)產(chǎn)生副本俯萎,以保證共享變量的可見(jiàn)性,局部阻止了指令重排的發(fā)生运杭。由此可見(jiàn)夫啊,在使用單例設(shè)計(jì)模式時(shí),即使是雙檢鎖也不一定會(huì)拿到最新的數(shù)據(jù)辆憔。

如下示例代碼在高并發(fā)場(chǎng)景中會(huì)存在問(wèn)題:


image.png

image.png

使用者在調(diào)用getTransactionService()時(shí)撇眯,有可能會(huì)得到初始化未完成的對(duì)象,究其原因虱咧,與java虛擬機(jī)的編譯優(yōu)化有關(guān)熊榛。對(duì)java虛擬機(jī)而言,初始化TransactionService實(shí)例和將對(duì)象地址寫到service字段并非原子操作腕巡,且這兩個(gè)階段的執(zhí)行順序是未定義的玄坦。假設(shè)某個(gè)線程執(zhí)行new TransactionService()時(shí),構(gòu)造方法還未被調(diào)用绘沉,編譯器僅僅為該對(duì)象分配了內(nèi)存空間并設(shè)為默認(rèn)值煎楣,此時(shí)若另一個(gè)線程調(diào)用getTransactionService()方法,由于service梆砸!= null转质,但是此時(shí)service對(duì)象還沒(méi)有被賦予真正有效的值,從而無(wú)法獲取到正確的service單例對(duì)象帖世。這就是著名的雙重檢查鎖定(Doubble-checked Locking)問(wèn)題休蟹,對(duì)象引用在沒(méi)有同步的情況下進(jìn)行讀操作,導(dǎo)致用戶可能會(huì)獲取未構(gòu)造完成的對(duì)象日矫。對(duì)于此問(wèn)題赂弓,一種較為簡(jiǎn)單的解決方案是用volatile關(guān)鍵字修飾目標(biāo)屬性(適用于JDK5及以上版本),這樣service就限制了編譯器對(duì)它的相關(guān)讀寫操作哪轿,對(duì)它的讀寫操作進(jìn)行指令重排盈魁,確定對(duì)象實(shí)例化之后才返回引用。鎖也可以確保變量的可見(jiàn)性窃诉,但是實(shí)現(xiàn)方式和volatile略有不同杨耙。線程在得到鎖時(shí)讀入副本,釋放時(shí)寫回內(nèi)存飘痛,鎖的操作尤其要符合happen before原則珊膜。

Volatile解決的是多線程共享變量的可見(jiàn)性問(wèn)題,類似于synchronized宣脉,但不具備synchronized的互斥性车柠。所以對(duì)volatile變量的操作并非都具有原子性,這是一個(gè)容易犯錯(cuò)誤的地方。一個(gè)線程對(duì)共享變量進(jìn)行10000次i++操作竹祷,另一個(gè)線程進(jìn)行10000次i--操作谈跛,如下示例代碼:


image.png

image.png

多次執(zhí)行后,發(fā)現(xiàn)結(jié)果基本都不為0.如果在count++和count--兩處都進(jìn)行加鎖操作塑陵,才會(huì)得到預(yù)期是0的結(jié)果感憾。這里對(duì)count的讀取、加1操作的字節(jié)碼如下:


image.png

需要4步才能完成加1操作猿妈。在該過(guò)程中吹菱,其他線程有足夠的時(shí)間覆蓋變量的值,如果想讓示例代碼最后的結(jié)果為零彭则,需要對(duì)count++和count--加鎖:
image.png

能實(shí)現(xiàn)count++原子操作的其他類有AtomicLong和LongAddr。JDK8推薦使用LongAddr類占遥,它比AtomicLong性能更好俯抖,有效地減少了樂(lè)觀鎖的重試次數(shù)。

因此瓦胎,“volatile是輕量級(jí)的同步方式”這種說(shuō)法是錯(cuò)誤的芬萍。它只是輕量級(jí)的線程操作可見(jiàn)方式,并非同步方式搔啊,如果是多寫場(chǎng)景柬祠,一定會(huì)產(chǎn)生線程安全問(wèn)題。如果是一寫多讀的并發(fā)場(chǎng)景负芋,使用volatile修飾變量則非常合適漫蛔。Volatile一寫多讀最典型的應(yīng)用時(shí)CopyOnWriteArrayList。它在修改數(shù)據(jù)時(shí)會(huì)把整個(gè)集合的數(shù)據(jù)全部復(fù)制出來(lái)旧蛾,對(duì)寫操作加鎖莽龟,修改完成后,再用setArray()把a(bǔ)rray指向新的集合锨天。使用volatile可以使讀線程盡快地感知array的修改毯盈,不進(jìn)行指令重排,操作后即可對(duì)其他線程可見(jiàn)病袄。源碼如下:


image.png

在實(shí)際業(yè)務(wù)中搂赋,如何清晰地判斷一寫多讀的場(chǎng)景顯得尤為重要。如果不確定共享變量是否會(huì)被多個(gè)線程并發(fā)寫益缠,保險(xiǎn)的做法是使用同步代碼塊來(lái)實(shí)現(xiàn)線程同步脑奠。另外,因?yàn)樗械牟僮鞫夹枰浇o內(nèi)存變量左刽,所以volatile一定會(huì)使線程的執(zhí)行速度變慢捺信,故要審慎定義和使用volatile屬性。

3信號(hào)量同步

信號(hào)量同步是指在不同的線程之間,通過(guò)傳遞同步信號(hào)量來(lái)協(xié)調(diào)線程執(zhí)行的先后次序迄靠。這里重點(diǎn)分析基于時(shí)間維度和信號(hào)維度的兩個(gè)類:CountDownLatch秒咨、Semaphore。

某國(guó)際化基礎(chǔ)語(yǔ)言管理平臺(tái)收到一個(gè)多語(yǔ)言翻譯請(qǐng)求后掌挚,根據(jù)目標(biāo)語(yǔ)種拆分成多個(gè)子線程雨席,對(duì)翻譯引擎發(fā)起翻譯請(qǐng)求。翻譯完成后吠式,同步返回給調(diào)用方陡厘,結(jié)果由于countDown()拋出異常,導(dǎo)致發(fā)生故障特占,警示代碼如下:


image.png

image.png

代碼中第1處拋出異常糙置,且該異常沒(méi)有被主線程try-catch到,最終該線程沒(méi)有執(zhí)行countDown()方法是目。程序執(zhí)行的時(shí)間較長(zhǎng)谤饭,該問(wèn)題難以定位,因?yàn)楫惓1煌痰靡桓啥舭媚伞U(kuò)展說(shuō)明一下揉抵,子線程異常可以通過(guò)線程方法setUncaughtExceptionHandler()捕獲嗤疯。

CountDownLatch是基于執(zhí)行時(shí)間的同步類冤今。在實(shí)際編碼中,可能需要處理基于空閑信號(hào)的同步情況茂缚。比如海關(guān)安檢的場(chǎng)景戏罢,任何國(guó)家公民在出國(guó)時(shí),都要走海關(guān)的查驗(yàn)通道阱佛。假設(shè)某機(jī)場(chǎng)的海關(guān)通道共有三個(gè)窗口帖汞,一批需要出關(guān)的人排成長(zhǎng)隊(duì),每個(gè)人都是一個(gè)線程凑术。當(dāng)3個(gè)窗口中的任意一個(gè)出現(xiàn)空閑時(shí)翩蘸,工作人員指示隊(duì)列中第一個(gè)人出隊(duì)到該空閑窗口接收檢查。對(duì)于上述場(chǎng)景淮逊,JDK中提供了一個(gè)Semaphore的信號(hào)同步類催首,只有在調(diào)用Semaphore對(duì)象的acquire()成功后,才可以往下執(zhí)行泄鹏,完成后執(zhí)行release()釋放持有的信號(hào)量郎任,下一個(gè)線程就可以馬上獲取這個(gè)空閑信號(hào)量進(jìn)入執(zhí)行”缸眩基于Semaphore的示例代碼如下:


image.png

image.png

如果某個(gè)人身份可疑舶治,需要確認(rèn)更多的信息分井,這不會(huì)影響到其他窗口的安檢速度。只要其他線程能夠拿到空閑信號(hào)霉猛,都可以馬上執(zhí)行尺锚。如果Semaphore的窗口信號(hào)量等于2,就是最典型的互斥鎖惜浅。

還有其他同步方式瘫辩,如CyclicBarrier是基于同步到達(dá)某個(gè)點(diǎn)的信號(hào)量觸發(fā)機(jī)制。CyclicBarrier從命名上即可知道它是一個(gè)可以循環(huán)使用(Cyclic)的屏障式(Barrier)多線程協(xié)作方式坛悉。采用這種方式進(jìn)行剛才的安檢服務(wù)伐厌,就是3個(gè)人同時(shí)進(jìn)去,只有3個(gè)人都完成安檢裸影,才會(huì)放下一批進(jìn)來(lái)挣轨。這是一種非常低效的安檢方式。但在某種場(chǎng)景下就是非常正確的方式空民,假設(shè)在機(jī)場(chǎng)排隊(duì)打車時(shí)刃唐,現(xiàn)場(chǎng)工作人員統(tǒng)一指揮,每次放3輛車進(jìn)來(lái)界轩,坐滿后開(kāi)走,再放下一批車和人進(jìn)來(lái)衔瓮。通過(guò)CyclicBarrier的reset()來(lái)釋放線程資源浊猾。

最后溫馨提示,無(wú)論從性能還是安全性上考慮热鞍,我們進(jìn)來(lái)使用并發(fā)包中提供的信號(hào)同步類葫慎,避免使用對(duì)象的wait()和notify()方式來(lái)進(jìn)行同步。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末薇宠,一起剝皮案震驚了整個(gè)濱河市偷办,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌澄港,老刑警劉巖椒涯,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異回梧,居然都是意外死亡废岂,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門狱意,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)湖苞,“玉大人,你說(shuō)我怎么就攤上這事详囤〔乒牵” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)隆箩。 經(jīng)常有香客問(wèn)我该贾,道長(zhǎng),這世上最難降的妖魔是什么摘仅? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任靶庙,我火速辦了婚禮,結(jié)果婚禮上娃属,老公的妹妹穿的比我還像新娘六荒。我一直安慰自己,他們只是感情好矾端,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布掏击。 她就那樣靜靜地躺著,像睡著了一般秩铆。 火紅的嫁衣襯著肌膚如雪砚亭。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,521評(píng)論 1 304
  • 那天殴玛,我揣著相機(jī)與錄音捅膘,去河邊找鬼。 笑死滚粟,一個(gè)胖子當(dāng)著我的面吹牛寻仗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播凡壤,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼署尤,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了亚侠?” 一聲冷哼從身側(cè)響起曹体,我...
    開(kāi)封第一講書(shū)人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎硝烂,沒(méi)想到半個(gè)月后箕别,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钢坦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年究孕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片爹凹。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡厨诸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出禾酱,到底是詐尸還是另有隱情微酬,我是刑警寧澤绘趋,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站颗管,受9級(jí)特大地震影響陷遮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜垦江,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一帽馋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧比吭,春花似錦绽族、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至赏表,卻和暖如春检诗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瓢剿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工逢慌, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人间狂。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓涕癣,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親前标。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355

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