01 |可見性蒋纬、原子性和有序性問題:并發(fā)編程Bug的源頭
原子性:線程切換導(dǎo)致原子性。
可見性:CPU緩存導(dǎo)致可見性来庭。
有序性:編譯優(yōu)化導(dǎo)致有序性宣决。
02 | Java內(nèi)存模型:看Java如何解決可見性和有序性問題
Java 內(nèi)存模型規(guī)范了 JVM 如何提供按需禁用緩存和編譯優(yōu)化的方法。具體來說吆你,這些方法包括 volatile弦叶、synchronized 和 final 三個關(guān)鍵字,以及六項 Happens-Before 規(guī)則妇多。
volatile:告訴編譯器伤哺,對這個變量的讀寫,不能使用 CPU 緩存者祖,必須從內(nèi)存中讀取或者寫入默责。
Happens-Before 規(guī)則
前面一個操作的結(jié)果對后續(xù)操作是可見的。在 Java 語言里面咸包,Happens-Before 的語義本質(zhì)上是一種可見性,A Happens-Before B 意味著 A 事件對 B 事件來說是可見的杖虾,無論 A 事件和 B 事件是否發(fā)生在同一個線程里烂瘫。
1、程序順序性原則?
按照程序順序奇适,前面的操作 Happens-Before 于后續(xù)的任意操作坟比。(此處指統(tǒng)一代碼塊中的單線程思維)
2、volatile原則
對一個 volatile 變量的寫操作嚷往, Happens-Before 于后續(xù)對這個 volatile 變量的讀操作葛账。(結(jié)合規(guī)則3(傳遞性)使用)(寫變量“v=true”? ? 讀變量 “v=true”)
3、傳遞性
如果 A Happens-Before B皮仁,且 B Happens-Before C籍琳,那么 A Happens-Before C。
舉例如下:
代碼:class VolatileExample {
? int x = 0;
? volatile boolean v = false;
? public void writer() {
? ? x = 42;
? ? v = true;
? }
? public void reader() {
? ? if (v == true) {
? ? ? // 這里x會是多少呢贷祈?
? ? }
? }
}
分析:上面的示例代碼趋急,假設(shè)線程 A 執(zhí)行 writer() 方法,按照 volatile 語義势誊,會把變量 “v=true” 寫入內(nèi)存呜达;假設(shè)線程 B 執(zhí)行 reader() 方法,同樣按照 volatile 語義粟耻,線程 B 會從內(nèi)存中讀取變量 v查近,如果線程 B 看到 “v == true” 時眉踱,那么線程 B 看到的變量 x 是42
PS:利用了volatite和傳遞性原則
4、管程中鎖的規(guī)則
對一個鎖的解鎖 Happens-Before 于后續(xù)對這個鎖的加鎖——(假設(shè) x 的初始值是 10霜威,線程 A 執(zhí)行完代碼塊后 x 的值會變成 12(執(zhí)行完自動釋放鎖)谈喳,線程 B 進(jìn)入代碼塊時,能夠看到線程 A 對 x 的寫操作侥祭,也就是線程 B 能夠看到 x==12)叁执。這個地方對于synchronized(管程中的一種)同樣適用,一個鎖的解鎖?Happens-Before后續(xù)這個鎖的加鎖矮冬。
5谈宛、線程start()原則
它是指主線程 A 啟動子線程 B 后,子線程 B 能夠看到主線程在啟動子線程 B 前的操作
6胎署、線程join原則
主線程 A 等待子線程 B 完成(主線程 A 通過調(diào)用子線程 B 的 join() 方法實現(xiàn))吆录,當(dāng)子線程 B 完成后(主線程 A 中 join() 方法返回),主線程能夠看到子線程的操作琼牧。當(dāng)然所謂的“看到”恢筝,指的是對共享變量的操作。
final
在 1.5 以后 Java 內(nèi)存模型對 final 類型變量的重排進(jìn)行了約束【薹唬現(xiàn)在只要我們提供正確構(gòu)造函數(shù)沒有“逸出”撬槽,就不會出問題了。
03 | 互斥鎖(上):解決原子性問題
我們把一段需要互斥執(zhí)行的代碼稱為臨界區(qū)趾撵。
Java 語言提供的鎖技術(shù):synchronized(鎖和鎖要保護的資源是有對應(yīng)關(guān)系的侄柔,比如你用你家的鎖保護你家的東西,我用我家的鎖保護我家的東西)占调。受保護資源和鎖之間的關(guān)聯(lián)關(guān)系是 N:1 的關(guān)系暂题,但是不能用多把鎖來保護一個資源。
當(dāng)修飾靜態(tài)方法的時候究珊,鎖定的是當(dāng)前類的 Class 對象薪者,在上面的例子中就是 Class X;當(dāng)修飾非靜態(tài)方法的時候剿涮,鎖定的是當(dāng)前實例對象 this言津。
同一線程在調(diào)用自己類中其他 synchronized 方法/塊或調(diào)用父類的 synchronized 方法/塊都不會阻礙該線程的執(zhí)行。就是說同一線程對同一個對象鎖是可重入的取试,而且同一個線程可以獲取同一把鎖多次纺念,也就是可以多次重入。
04 | 互斥鎖(下):如何用一把鎖保護多個資源想括?
細(xì)粒度鎖:用不同的鎖對受保護資源進(jìn)行精細(xì)化管理陷谱,是性能優(yōu)化的一個重要手段。(使用細(xì)粒度鎖是有代價的,這個代價就是可能會導(dǎo)致死鎖)
this 這把鎖可以保護自己的資源烟逊,卻保護不了別人的資源渣窜,就像你不能用自家的鎖來保護別人家的資產(chǎn),也不能用自己的票來保護別人的座位一樣宪躯。
我們提到用同一把鎖來保護多個資源——包場:鎖能覆蓋所有受保護資源
“原子性”的本質(zhì):其實不是不可分割乔宿,不可分割只是外在表現(xiàn),其本質(zhì)是多個資源間有一致性的要求访雪,操作的中間狀態(tài)對外不可見详瑞。(解決原子性問題,是要保證中間狀態(tài)對外不可見臣缀。)
PS:不能用可變對象做鎖
05 | 一不小心就死鎖了坝橡,怎么辦?
死鎖的一個比較專業(yè)的定義是:一組互相競爭資源的線程因互相等待精置,導(dǎo)致“永久”阻塞的現(xiàn)象计寇。
以下四個條件都發(fā)生時才會出現(xiàn)死鎖:
1、互斥脂倦,共享資源 X 和 Y 只能被一個線程占用番宁;
2、占有且等待赖阻,線程 T1 已經(jīng)取得共享資源 X蝶押,在等待共享資源 Y 的時候,不釋放共享資源 X火欧;
3棋电、不可搶占,其他線程不能強行搶占線程 T1 占有的資源布隔;
4、循環(huán)等待稼虎,線程 T1 等待線程 T2 占有的資源衅檀,線程 T2 等待線程 T1 占有的資源,就是循環(huán)等待霎俩。
也就是說只要我們破壞其中一個哀军,就可以成功避免死鎖的發(fā)生(互斥這個條件我們沒有辦法破壞,因為我們用鎖為的就是互斥)
1. 破壞占用且等待條件
從理論上講打却,要破壞這個條件杉适,可以一次性申請所有資源。
2. 破壞不可搶占條件
核心是要能夠主動釋放它占有的資源
3. 破壞循環(huán)等待條件
破壞這個條件柳击,需要對資源進(jìn)行排序猿推,然后按序申請資源。(這個實現(xiàn)非常簡單,我們假設(shè)每個賬戶都有不同的屬性 id蹬叭,這個 id 可以作為排序字段藕咏,申請的時候,我們可以按照從小到大的順序來申請秽五。)
06 | 用“等待-通知”機制優(yōu)化循環(huán)等待
等待 - 通知機制:線程首先獲取互斥鎖孽查,當(dāng)線程要求的條件不滿足時,釋放互斥鎖坦喘,進(jìn)入等待狀態(tài)盲再;當(dāng)要求的條件滿足時,通知等待的線程瓣铣,重新獲取互斥鎖答朋。
Java 語言內(nèi)置的 synchronized 配合 wait()、notify()坯沪、notifyAll() 這三個方法就能輕松實現(xiàn)等待 - 通知機制
等待隊列和互斥鎖是一對一的關(guān)系绿映,每個互斥鎖都有自己獨立的等待隊列。
wait() 操作工作原理圖
notify() 操作工作原理圖
注意:被通知的線程要想重新執(zhí)行腐晾,仍然需要獲取到互斥鎖(因為曾經(jīng)獲取的鎖在調(diào)用 wait() 時已經(jīng)釋放了)
等待 - 通知機制中叉弦,我們需要考慮以下四個要素。
互斥鎖:上一篇文章我們提到 Allocator 需要是單例的藻糖,所以我們可以用 this 作為互斥鎖淹冰。
線程要求的條件:轉(zhuǎn)出賬戶和轉(zhuǎn)入賬戶都沒有被分配過。
何時等待:線程要求的條件不滿足就等待巨柒。
何時通知:當(dāng)有線程釋放賬戶時就通知樱拴。
07 | 安全性、活躍性以及性能問題
并發(fā)編程主要要注意三個問題:安全性問題洋满、活躍性問題和性能問題
安全性問題
線程安全:程序按照我們期望的執(zhí)行晶乔,不要讓我們感到意外
數(shù)據(jù)競爭:當(dāng)多個線程同時訪問同一數(shù)據(jù)
競態(tài)(爭)條件:指的是程序的執(zhí)行結(jié)果依賴線程執(zhí)行的順序
活躍性問題
活躍性問題:指的是某個操作無法執(zhí)行下去。
“死鎖”就是一種典型的活躍性問題牺勾,當(dāng)然除了死鎖外正罢,還有兩種情況,分別是“活鎖”和“饑餓”驻民。
有時線程雖然沒有發(fā)生阻塞翻具,但仍然會存在執(zhí)行不下去的情況,這就是所謂的“活鎖”
“饑餓”指的是線程因無法訪問所需資源而無法執(zhí)行下去的情況——在 CPU 繁忙的情況下回还,優(yōu)先級低的線程得到執(zhí)行的機會很小裆泳,就可能發(fā)生線程“饑餓”;持有鎖的線程柠硕,如果執(zhí)行的時間過長工禾,也可能導(dǎo)致“饑餓”問題。——解決“饑餓”問題的方案帜篇,有三種方案:一是保證資源充足糙捺,二是公平地分配資源,三就是避免持有鎖的線程長時間執(zhí)行笙隙。這三個方案中洪灯,方案一和方案三的適用場景比較有限,因為很多場景下竟痰,資源的稀缺性是沒辦法解決的签钩,持有鎖的線程執(zhí)行的時間也很難縮短。倒是方案二的適用場景相對來說更多一些坏快。那如何公平地分配資源呢铅檩?在并發(fā)編程里,主要是使用公平鎖
性能問題
Java SDK 并發(fā)包里之所以有那么多東西莽鸿,有很大一部分原因就是要提升在某個特定領(lǐng)域的性能昧旨。
第一,既然使用鎖會帶來性能問題祥得,那最好的方案自然就是使用無鎖的算法和數(shù)據(jù)結(jié)構(gòu)了兔沃。
第二,減少鎖持有的時間级及。
性能方面的度量指標(biāo)有很多乒疏,我覺得有三個指標(biāo)非常重要,就是:吞吐量饮焦、延遲和并發(fā)量
08 | 管程:并發(fā)編程的萬能鑰匙
管程(Monitor)和信號量是等價的怕吴,所謂等價指的是用管程能夠?qū)崿F(xiàn)信號量,也能用信號量實現(xiàn)管程县踢。
管程转绷,指的是管理共享變量以及對共享變量的操作過程,讓他們支持并發(fā)硼啤∫榫——翻譯為 Java 領(lǐng)域的語言,就是管理類的成員變量和成員方法丙曙,讓這個類是線程安全的爸业。
MESA 模型
在并發(fā)編程領(lǐng)域其骄,有兩大核心問題:一個是互斥亏镰,即同一時刻只允許一個線程訪問共享資源;另一個是同步拯爽,即線程之間如何通信索抓、協(xié)作。
管程解決互斥問題的思路很簡單,就是將共享變量及其對共享變量的操作統(tǒng)一封裝起來逼肯。
封裝變量&封裝對變量的操作
借鑒就醫(yī)流程耸黑,看管程如何解決線程間的同步問題
MEAS管程模型
每個條件變量都對應(yīng)有一個等待隊列
兩個重要的類:ReentrantLock &? Condition
final Lock lock = new ReentrantLock();
// 條件變量:隊列不滿 final Condition notFull = lock.newCondition();
// 條件變量:隊列不空 final Condition notEmpty = lock.newCondition();
09 | Java線程(上):Java線程的生命周期
通用的線程生命周期基本上可以用下圖這個“五態(tài)模型”來描述。這五態(tài)分別是:初始狀態(tài)篮幢、可運行狀態(tài)大刊、運行狀態(tài)、休眠狀態(tài)和終止?fàn)顟B(tài)三椿。
通用線程狀態(tài)轉(zhuǎn)換圖——五態(tài)模型
Java 語言中線程共有六種狀態(tài)缺菌,分別是:
NEW(初始化狀態(tài))
RUNNABLE(可運行 / 運行狀態(tài))
BLOCKED(阻塞狀態(tài))
WAITING(無時限等待)
TIMED_WAITING(有時限等待)
TERMINATED(終止?fàn)顟B(tài))
Java 線程中的 BLOCKED、WAITING搜锰、TIMED_WAITING 是一種狀態(tài)伴郁,即前面我們提到的休眠狀態(tài)。也就是說只要 Java 線程處于這三種狀態(tài)之一蛋叼,那么這個線程就永遠(yuǎn)沒有 CPU 的使用權(quán)焊傅。
Java 中的線程狀態(tài)轉(zhuǎn)換圖
1. RUNNABLE 與 BLOCKED 的狀態(tài)轉(zhuǎn)換
只有一種場景會觸發(fā)這種轉(zhuǎn)換,就是線程等待 synchronized 的隱式鎖(等待的線程就會從 RUNNABLE 轉(zhuǎn)換到 BLOCKED 狀態(tài)狈涮。而當(dāng)?shù)却木€程獲得 synchronized 隱式鎖時狐胎,就又會從 BLOCKED 轉(zhuǎn)換到 RUNNABLE 狀態(tài)。)
2. RUNNABLE 與 WAITING 的狀態(tài)轉(zhuǎn)換
Object.wait() 薯嗤、 Thread.join() 顽爹、 LockSupport.park()
3. RUNNABLE 與 TIMED_WAITING 的狀態(tài)轉(zhuǎn)換
調(diào)用帶超時參數(shù)的 Thread.sleep(long millis) 方法;獲得 synchronized 隱式鎖的線程骆姐,調(diào)用帶超時參數(shù)的 Object.wait(long timeout) 方法镜粤;調(diào)用帶超時參數(shù)的 Thread.join(long millis) 方法;調(diào)用帶超時參數(shù)的 LockSupport.parkNanos(Object blocker, long deadline) 方法玻褪;調(diào)用帶超時參數(shù)的 LockSupport.parkUntil(long deadline) 方法肉渴。
TIMED_WAITING 和 WAITING 狀態(tài)的區(qū)別,僅僅是觸發(fā)條件多了超時參數(shù)带射。
4. 從 NEW 到 RUNNABLE 狀態(tài)
從 NEW 狀態(tài)轉(zhuǎn)換到 RUNNABLE 狀態(tài)很簡單同规,調(diào)用線程對象的 start()
5. 從 RUNNABLE 到 TERMINATED 狀態(tài)
1、線程執(zhí)行完 run() 方法后窟社,會自動轉(zhuǎn)換到 TERMINATED 狀態(tài)券勺,當(dāng)然如果執(zhí)行 run() 方法的時候異常拋出,也會導(dǎo)致線程終止灿里。? 2关炼、調(diào)用 interrupt() 方法
6、NEW狀態(tài)
新建一個線程對象
010 | Java線程(中):創(chuàng)建多少線程才是合適的匣吊?
要解決這個問題儒拂,首先要分析以下兩個問題:
為什么要使用多線程寸潦?? 多線程的應(yīng)用場景有哪些?
計算機主要有哪些硬件呢社痛?主要是兩類:一個是 I/O见转,一個是 CPU。簡言之蒜哀,在并發(fā)編程領(lǐng)域斩箫,提升性能本質(zhì)上就是提升硬件的利用率,再具體點來說撵儿,就是提升 I/O 的利用率和 CPU 的利用率校焦。(PS: CPUworking跟I/Oworking都是有線程在參與)
我們的程序一般都是 CPU 計算和 I/O 操作交叉執(zhí)行的,所以有CPU密集型跟IO密集型的區(qū)別
CPU 密集型計算
對于 CPU 密集型的計算場景统倒,理論上“線程的數(shù)量 =CPU 核數(shù)”就是最合適的寨典。不過在工程上,線程的數(shù)量一般會設(shè)置為“CPU 核數(shù) +1”房匆,這樣的話耸成,當(dāng)線程因為偶爾的內(nèi)存頁失效或其他原因?qū)е伦枞麜r,這個額外的線程可以頂上浴鸿,從而保證 CPU 的利用率井氢。
I/O 密集型的計算場景
最佳線程數(shù) =1 +(I/O 耗時 / CPU 耗時)? ? 單核CPU
最佳線程數(shù) =CPU 核數(shù) * [ 1 +(I/O 耗時 / CPU 耗時)? ? 多核CPU
目標(biāo)就是讓CPU 和 I/O 設(shè)備的利用率都達(dá)到最高位。將硬件的性能發(fā)揮到極致岳链。
單線程執(zhí)行示意圖
二線程執(zhí)行示意圖
三線程執(zhí)行示意圖
011 | Java線程(下):為什么局部變量是線程安全的花竞?
局部變量的作用域是方法內(nèi)部,也就是說當(dāng)方法執(zhí)行完掸哑,局部變量就沒用了约急,局部變量應(yīng)該和方法同生共死。(局部變量是和方法同生共死的苗分,一個變量如果想跨越方法的邊界厌蔽,就必須創(chuàng)建在堆里。)
兩個線程可以同時用不同的參數(shù)調(diào)用相同的方法摔癣,那調(diào)用棧和線程之間是什么關(guān)系呢奴饮?答案是:每個線程都有自己獨立的調(diào)用棧。
線程與調(diào)用棧的關(guān)系圖
沒有共享择浊,就沒有傷害戴卜。
附:
當(dāng)調(diào)用方法時,會創(chuàng)建新的棧幀琢岩,并壓入調(diào)用棧投剥;
調(diào)用棧結(jié)構(gòu)
局部變量就是放到了調(diào)用棧里
保護局部變量的調(diào)用棧結(jié)構(gòu)
012 |? 如何用面向?qū)ο笏枷雽懞貌l(fā)程序?
在 Java 語言里粘捎,面向?qū)ο笏枷肽軌蜃尣l(fā)編程變得更簡單薇缅。
用面向?qū)ο笏枷雽懞貌l(fā)程序:
1、封裝共享變量? 2攒磨、識別共享變量間的約束條件? 3泳桦、制定并發(fā)訪問策略
1、封裝共享變量
面向?qū)ο笏枷肜锩嬗幸粋€很重要的特性是封裝娩缰,封裝的通俗解釋就是將屬性和實現(xiàn)細(xì)節(jié)封裝在對象內(nèi)部灸撰,外界對象只能通過目標(biāo)對象提供的公共方法來間接訪問這些內(nèi)部屬性.將共享變量作為對象屬性封裝在內(nèi)部,對所有公共方法制定并發(fā)訪問策略拼坎。
對于不會發(fā)生變化的共享變量浮毯,建議你用 final 關(guān)鍵字來修飾。
2泰鸡、識別共享變量間的約束條件
這些約束條件债蓝,決定了并發(fā)訪問策略。識別出所有共享變量之間的約束條件盛龄,如果約束條件識別不足饰迹,很可能導(dǎo)致制定的并發(fā)訪問策略南轅北轍。
共享變量之間的約束條件余舶,反映在代碼里啊鸭,基本上都會有 if 語句,所以匿值,一定要特別注意競態(tài)條件赠制。
3、制定并發(fā)訪問策略
避免共享:避免共享的技術(shù)主要是利于線程本地存儲以及為每個任務(wù)分配獨立的線程挟憔。
不變模式:這個在 Java 領(lǐng)域應(yīng)用的很少钟些,但在其他領(lǐng)域卻有著廣泛的應(yīng)用,例如 Actor 模式绊谭、CSP 模式以及函數(shù)式編程的基礎(chǔ)都是不變模式厘唾。
管程及其他同步工具:Java 領(lǐng)域萬能的解決方案是管程,但是對于很多特定場景龙誊,使用 Java 并發(fā)包提供的讀寫鎖抚垃、并發(fā)容器等同步工具會更好。
優(yōu)先使用成熟的工具類:Java SDK 并發(fā)包里提供了豐富的工具類趟大,基本上能滿足你日常的需要鹤树,建議你熟悉它們,用好它們逊朽,而不是自己再“發(fā)明輪子”罕伯,畢竟并發(fā)工具類不是隨隨便便就能發(fā)明成功的。
迫不得已時才使用低級的同步原語:低級的同步原語主要指的是 synchronized叽讳、Lock追他、Semaphore 等坟募,這些雖然感覺簡單,但實際上并沒那么簡單邑狸,一定要小心使用懈糯。
避免過早優(yōu)化:安全第一,并發(fā)程序首先要保證安全单雾,出現(xiàn)性能瓶頸后再優(yōu)化赚哗。在設(shè)計期和開發(fā)期,很多人經(jīng)常會情不自禁地預(yù)估性能的瓶頸硅堆,并對此實施優(yōu)化屿储,但殘酷的現(xiàn)實卻是:性能瓶頸不是你想預(yù)估就能預(yù)估的。