前言
高并發(fā)萄窜、多線程可謂是 Java 最核心诈泼、最難懂唧垦,也是圈子里都趨之若鶩的知識(shí)點(diǎn)捅儒,掌握這一知識(shí)就可以在工作和面試中站在“上帝視角”笑傲江湖。筆者將會(huì)再次從 JMM 和 JVM 出發(fā)振亮,從緩存一致性出發(fā)巧还,再到 volatile
、然后講解 synchronized
的實(shí)現(xiàn)原理双炕、Lock
的最佳實(shí)踐狞悲,最后結(jié)合自己的實(shí)踐經(jīng)驗(yàn)談?wù)勛约簩?duì)并發(fā)的認(rèn)識(shí)。
基礎(chǔ)知識(shí)準(zhǔn)備
Amdahl 定律和 Gustafson 定律
先來說一下并發(fā)編程下兩條重要的性能定律:Amdahl(阿姆達(dá)爾)定律和 Gustafson(古斯塔夫森)定律妇斤。Amdahl 說的是在一個(gè)應(yīng)用程序中并行數(shù)量的百分比越高摇锋,其整體的性能就會(huì)越來越高,Gustafon 說的是在應(yīng)用程序中如果串行數(shù)量一定的情況下站超,并行的數(shù)量不論怎么增長荸恕,都會(huì)達(dá)到一個(gè)極限。舉例說明就是死相,A 到 B 的距離是 100KM融求,前半程速度是 50KM/h,那么在后半程速度越高算撮,那么整體的花費(fèi)的時(shí)間就會(huì)越短生宛,這就是 Amdahl 定律,然而不論后半程速度不論快肮柜,哪怕是光速陷舅,其花費(fèi)的時(shí)間也只能是無限接近 1h,這就是 Gustafon 定律审洞。
線程通信機(jī)制
在線程之間的通信機(jī)制有兩種莱睁,共享內(nèi)存和消息傳遞。在共享內(nèi)存的方式中,線程之間通過 read-write 內(nèi)存中的公共狀態(tài)隱式實(shí)現(xiàn)通信仰剿,典型的作法就是通過共享對(duì)象的方式進(jìn)行通信创淡。但是在繁忙的內(nèi)存操作中,如何實(shí)現(xiàn)共享方式南吮,就需要依靠緩存一致性協(xié)議琳彩,具體實(shí)現(xiàn)就是 volatile 關(guān)鍵字。在消息傳遞的方式中旨袒,線程之間需要顯式地發(fā)送消息來實(shí)現(xiàn)通信汁针,這就是為什么需要有 wait 和 notify 方法。
線程和進(jìn)程
擁有資源和獨(dú)立調(diào)度的基本單位是進(jìn)程砚尽。在操作系統(tǒng)中施无,線程是獨(dú)立調(diào)度的基本單位,進(jìn)程是資源擁有的基本單位必孤。調(diào)度 CPU 資源的最小單位是線程猾骡,線程又分為線用戶級(jí)線程和內(nèi)核級(jí)線程。
用戶級(jí)線程(User Level Thread敷搪,簡稱 ULT)
用戶級(jí)線程是由用戶創(chuàng)建的線程兴想,線程阻塞則其進(jìn)程阻塞,用戶線程比內(nèi)核線程創(chuàng)建速度赡勘、切換速度要快嫂便。用戶線程創(chuàng)建不需要依賴操作系統(tǒng)核心,內(nèi)核對(duì)用戶線程是無感知的闸与。
內(nèi)核級(jí)線程(Kernel Level Thread毙替,簡稱 KLT)
內(nèi)核級(jí)線程是由內(nèi)核創(chuàng)建的,內(nèi)核線程線程阻塞不會(huì)導(dǎo)致其進(jìn)程阻塞践樱,只有內(nèi)核線程才具有操作 CPU 的權(quán)限厂画,用戶級(jí)線程通過內(nèi)核提供的交互接口進(jìn)行數(shù)據(jù)交互,內(nèi)核級(jí)線程的創(chuàng)建比用戶級(jí)線程開銷更大拷邢。
Tips:用戶級(jí)線程和內(nèi)核級(jí)線程是隔離開的袱院,最直接的證據(jù)就是,在操作系統(tǒng)中某個(gè)用戶的線程報(bào)錯(cuò)會(huì)導(dǎo)致用戶進(jìn)停止瞭稼、異常忽洛,但是一般不會(huì)導(dǎo)致整個(gè)系統(tǒng)異常、宕機(jī)环肘。
串行欲虚、并行和并發(fā)
- 串行:一次只能取得一個(gè)任務(wù)并執(zhí)行這一個(gè)任務(wù)。
- 并行:多進(jìn)程或多線程的方式同時(shí)執(zhí)行這些任務(wù)廷臼,每個(gè)進(jìn)程或者每個(gè)線程分到一個(gè)任務(wù)。
- 并發(fā):同時(shí)運(yùn)行多個(gè)線程并將一個(gè)大任務(wù)拆解成多個(gè)子任務(wù)執(zhí)行,單個(gè)線程會(huì)分到多個(gè)子任務(wù)荠商。
死鎖和活鎖
死鎖
死鎖是兩個(gè)或者兩個(gè)以上的進(jìn)程或者線程在執(zhí)行過程中因爭奪資源而相互等待的現(xiàn)象寂恬,簡單的說法就是線程相互持有另一個(gè)線程所需要的資源,造成了線程間相互阻塞的情況莱没。
活鎖
活鎖是任務(wù)沒有被阻塞初肉,但是由于某些條件沒有滿足,導(dǎo)致線程重復(fù)嘗試 -> 失敗 -> 嘗試 -> 失敗的惡性循環(huán)中饰躲。簡單點(diǎn)說就是線程間相互的謙讓導(dǎo)致任務(wù)不能正常的執(zhí)行牙咏。
死鎖和活鎖的區(qū)別在于活鎖的情況下被鎖的對(duì)象狀態(tài)是不斷變化的(線程操作發(fā)現(xiàn)不能完全滿足條件而造成的對(duì)象的加鎖和解鎖),這就是活鎖的體現(xiàn)嘹裂。死鎖則表現(xiàn)在線程間相互等待妄壶,被鎖的對(duì)象狀態(tài)是一直不變的,線程間處于阻塞的狀態(tài)寄狼《〖模活鎖可能自行解鎖,死鎖則需要依靠外力進(jìn)行解鎖泊愧。
鎖饑餓
鎖饑餓則是一個(gè)或者多個(gè)線程因?yàn)槎喾N原因無法獲取資源伊磺,導(dǎo)致任務(wù)一直無法執(zhí)行。由于 Java 采用了時(shí)間片輪轉(zhuǎn)的方式實(shí)現(xiàn)線程調(diào)度删咱,因此可以對(duì)線程設(shè)置優(yōu)先級(jí)屑埋。比如高優(yōu)先級(jí)的線程一直搶占低優(yōu)先級(jí)線程的 CPU 時(shí)間,就會(huì)導(dǎo)致線程一直在被阻塞痰滋。
重排序
為了提高和優(yōu)化程序性能編譯器和處理器對(duì)指令序列進(jìn)行重新排序摘能,但是不論怎么改變,都必須要遵守 as-if-serial 語義和 happens-before 原則即寡。
as-if-serial 語義說的是在多線程的情況下徊哑,也能保證程序串行(或者說是單線程)的方式運(yùn)行。這里距一個(gè)例子:
// A 步驟給 x 賦值聪富,B 步驟給 x 賦值為 4莺丑,C 步驟給 x 賦值為 3
int x = 0; // A
x= 4; // B
x =3; // C
System.out.println("x = " + x); // D 輸出結(jié)果
如果發(fā)生指令重排,那么 C 可能出現(xiàn)在 B 的前面墩蔓,那么最后出現(xiàn)的結(jié)果就是錯(cuò)誤的值 4梢莽。
**happens-before**
是 JMM 最核心的概念,JSR-133 定義的 happens-before
的規(guī)則如下:
- 程序順序性:線程中的每個(gè)操作奸披,都要優(yōu)先于線程中的任意后續(xù)操作昏名。例如方法返回結(jié)果必須整個(gè)方法運(yùn)行結(jié)束后才能返回。
- 監(jiān)視器鎖規(guī)則:線程加鎖的動(dòng)作要先于其解鎖的動(dòng)作阵面。
- volatile 變量規(guī)則:volatile 的寫操作要優(yōu)先于 volatile 讀操作轻局。
- 傳遞性:如果 A 優(yōu)先于 B洪鸭,B 優(yōu)先于 C,那么 A 也一定優(yōu)先于 C 仑扑。
- start 原則:線程 start 之后览爵,線程才能執(zhí)行動(dòng)作。
逃逸分析
前一篇文章中也講到了如果需要棧上分配镇饮,則需要開啟逃逸分析蜓竹,這樣對(duì)象才能在棧上創(chuàng)建,這里再次詳細(xì)說一下開啟逃逸分析的特點(diǎn):
-
棧上分配:對(duì)象如果不會(huì)逃逸到方法外储藐,則對(duì)象可以在棧上進(jìn)行內(nèi)存分配俱济,棧銷毀會(huì)將對(duì)象銷毀,避免了垃圾回收操作钙勃,開啟逃逸分析的 JVM 參數(shù)配置
-XX:+DoEscapeAnalysis
蛛碌。 -
同步消除:對(duì)象如果不會(huì)逃逸出線程,可以將對(duì)象的同步操作消除肺缕,如果代碼里進(jìn)行了加鎖操作左医,JVM 會(huì)自動(dòng)去除加鎖操作。開啟同步消除的 JVM 參數(shù)配置
-XX:+EliminateLocks
同木。 -
標(biāo)量替換:聚合量簡單的理解就是一個(gè)對(duì)象浮梢,標(biāo)量就是基本數(shù)據(jù)類型和簡單的包裝類。標(biāo)量替換是將一個(gè)聚合量拆散成標(biāo)量的過程彤路,如果一個(gè)對(duì)象不會(huì)被外部訪問秕硝,且對(duì)象可以被拆散,那么程序執(zhí)行時(shí)不會(huì)創(chuàng)建此對(duì)象洲尊,而是用對(duì)象中的屬性標(biāo)量來替代整個(gè)對(duì)象远豺。開啟標(biāo)量替換的 JVM 參數(shù)配置
-XX:+EliminateAllocations
。
緩存一致性協(xié)議
這個(gè)知識(shí)點(diǎn)大家第一反應(yīng)不就是 MESI 嗎坞嘀?其實(shí)還真不是躯护,MESI 協(xié)議只是緩存一致性協(xié)議的其中一種實(shí)現(xiàn)方式而已,是 Intel 的提出的丽涩,也是目前認(rèn)知最廣的一種棺滞,還有 ASM 協(xié)議等。
大家都知道數(shù)據(jù)的讀寫速度如下:
讀 CPU > 讀緩存 > 讀內(nèi)存 > 讀硬盤 > 讀網(wǎng)絡(luò)
由于存在讀取速度的差異矢渊,所以會(huì)在 CPU 和內(nèi)存之間加上多級(jí)緩存继准,來解決讀寫速度差的問題,同時(shí)也提高了 CPU 和主存的工作效率矮男。但是多級(jí)緩存的存在移必,在多個(gè)線程競爭讀寫主存(物理內(nèi)存)當(dāng)中數(shù)據(jù)的時(shí)候,到導(dǎo)致數(shù)據(jù)不一致的嚴(yán)重后果毡鉴。假設(shè)主存中有一個(gè)變量 a =1崔泵,假設(shè) CPU1 和 CPU2 已經(jīng)對(duì)應(yīng)的多級(jí)緩存中已經(jīng)加載了 a 的副本秒赤,在 t0 時(shí)刻各個(gè)位置的 a 的值都為 1,在某一時(shí)刻 t1憎瘸,CPU1 將 a 的值 +1 并寫入緩存中倒脓,在 t2 時(shí)刻 CPU2 將 a 的值 +1 也寫入了緩存中,按照時(shí)間的順序含思,a 的值應(yīng)該變?yōu)?3,但是由于緩存的存在甘晤,在 t2 時(shí)刻 CPU2 對(duì)應(yīng)的緩存 a 的值并沒有更新為最新的值為 2含潘,因此,當(dāng)緩存同步數(shù)據(jù)到主存時(shí)线婚,最終的結(jié)果將會(huì)是 2遏弱。具體過程如下圖所示: