Synchronized
synchronized 可以用來修飾
- 方法
- 靜態(tài)方法
- 對(duì)象
但其實(shí)本質(zhì)就是 鎖對(duì)象脉课,修飾方法時(shí)鎖 this,修飾靜態(tài)方法時(shí)鎖 class财异,當(dāng)一個(gè)線程獲取了一個(gè)對(duì)象的鎖倘零,那么后到的線程都需要等待鎖被釋放
void synchronized syn() {...}
如上代碼其實(shí)等同于如下代碼
void syn() {
synchronized(this) {
// ...
}
}
對(duì)象鎖的狀態(tài)大致分為四種,鎖隨著競(jìng)爭(zhēng)激烈應(yīng)當(dāng)逐步升級(jí)戳寸,但不允許回退
- 無鎖
- 偏向鎖
- 輕量級(jí)鎖
- 重量級(jí)鎖
我們開始重點(diǎn)討論 synchronized 中鎖的實(shí)現(xiàn)以及加鎖策略之前呈驶,需要明白以下概念,在 Java 對(duì)象頭里的有一個(gè)字段叫 Mark Word疫鹊,其中存儲(chǔ)了對(duì)象的 HashCode袖瞻,分代年齡和 鎖信息,而鎖信息就代表著對(duì)象加的是一把什么鎖
互斥鎖
每個(gè)對(duì)象擁有一個(gè)屬于自己的寶貝 - monitor拆吆,它就是一把 互斥鎖聋迎,占有鎖時(shí) 排斥任何人
monitor 十分重量級(jí),每個(gè)對(duì)象誕生時(shí)枣耀,monitor 會(huì)被初始化為 0
monitor 是一種可重入的砌庄,重量級(jí)的互斥鎖,之所以稱之為 重量級(jí)奕枢,因?yàn)槠浠诒O(jiān)視器 monitor娄昆,而 monitor 本質(zhì)又依賴于底層操作系統(tǒng) Mutex Lock,線程的阻塞和喚醒需要CPU從用戶態(tài)轉(zhuǎn)為核心態(tài)缝彬,頻繁的阻塞和喚醒對(duì)CPU來說是一件負(fù)擔(dān)很重的工作,線程之間的切換成本非常高萌焰,應(yīng)此, monitor 應(yīng)當(dāng)成為同步的最后一種手段
獲取與釋放
對(duì)應(yīng)的虛擬機(jī)指令分別為 monitorenter 和 monitorexit
- monitorenter : 嘗試獲取該對(duì)象的鎖
- monitorexit : 釋放該對(duì)象鎖
如果我們有一個(gè)線程試圖占用對(duì)象谷浅,占用后進(jìn)行一定操作 some process扒俯,之后便結(jié)束占用,那么虛擬機(jī)指令是如此執(zhí)行的
// before monitorenter ...
monitorenter
// try own lock of object successfully and do something ...
monitorexit
// after monitorexit ...
線程嘗試獲取一把互斥鎖時(shí)一疯,需要經(jīng)過以下步驟
- 若 monitor = 0 撼玄,線程獲得一把互斥鎖,并且 monitor++
- 若 線程已擁有互斥鎖墩邀,仍然 monitor++
- 若 monitor != 0 并且 線程未擁有鎖掌猛,掛起線程,等待鎖被釋放
線程釋放一把互斥鎖時(shí)眉睹,需要經(jīng)過以下步驟
- monitorexit 后 monitor--
- monitor = 0 后荔茬,喚醒等待線程废膘,通知鎖已經(jīng)被釋放
自適應(yīng)自旋鎖
線程的阻塞、掛起和喚醒非常損耗性能慕蔚,耗時(shí)比同步代碼更長(zhǎng)的情況很可能出現(xiàn)丐黄,如果同步代碼執(zhí)行本來就比較短,那為何要進(jìn)行線程切換呢
自旋鎖就是為解決這個(gè)問題而出現(xiàn)的孔飒,當(dāng)線程嘗試獲取鎖失敗時(shí)灌闺,線程會(huì)自旋一段時(shí)間后,再嘗試獲取鎖坏瞄,如果失敗那再掛起
自旋過程中桂对,可以理解為等待鎖被釋放,自旋意思就是做其他事情等待鎖被釋放惦积,你可以坐著,躺著或干啥都行
自旋性能的關(guān)鍵在于 自旋時(shí)長(zhǎng) 如何決定猛频,JVM 中采用的是一種 自適應(yīng) 策略狮崩,即通過觀察以往自旋時(shí)間,若通過該自旋時(shí)間后線程獲取鎖成功鹿寻,那么下一次自旋這么久也很有可能成功睦柴,反著則反,若一直失敗毡熏,則直接取消自旋會(huì)更加劃算
鎖撤銷
當(dāng)虛擬機(jī)檢測(cè)到坦敌,同步的地方并沒有同步價(jià)值時(shí),編譯時(shí)會(huì)撤銷該鎖痢法,如下面代碼塊狱窘,則完全不需要同步,String 是不可變對(duì)象财搁,完全不可能被修改
public synchronized void print(String s) {
System.out.println(s);
}
鎖粗化
我們平時(shí)寫代碼一般要求將鎖細(xì)化蘸炸,即只講鎖加在需要同步的地方,然后鎖的獲取與釋放尖奔,像剛才提到的互斥鎖搭儒,是非常耗性能的,因此 當(dāng)可優(yōu)化的多段鎖出現(xiàn)時(shí)提茁,會(huì)粗化該鎖
public String plus(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
如上述語句可能并發(fā)現(xiàn)不了什么不得了的事情淹禾,但字符串的拼接是挺費(fèi)性能的,應(yīng)當(dāng)使用 StringBuilder 或者 StringBuffer茴扁,虛擬機(jī)則如下優(yōu)化
public String plus(String s1, String s2, String s3) {
StringBuffer buffer = new StringBuffer();
buffer.append(s1);
buffer.append(s2);
buffer.append(s3);
return buffer.toString();
}
StringBuffer 是同步的铃岔,所以拼接時(shí)會(huì)上三把鎖,然而一把鎖的性能會(huì)更好
輕量級(jí)鎖
使用重量級(jí)互斥鎖時(shí)峭火,我們應(yīng)當(dāng)考慮一個(gè)問題德撬,一把互斥鎖 并不是每時(shí)都在被多線程競(jìng)爭(zhēng)铲咨,在不被競(jìng)爭(zhēng)時(shí),排斥鎖的線程切換蜓洪,就顯得沒那么必要了纤勒,這就是一個(gè)優(yōu)化的點(diǎn),所以就引入了輕量級(jí)鎖
在決定使用底層操作系統(tǒng)的 monitor 之前隆檀,應(yīng)當(dāng)先考慮 輕量級(jí)鎖摇天,它不使用線程切換,但能達(dá)到同步目的
獲取與釋放
線程嘗試獲取一把輕量級(jí)鎖時(shí)恐仑,需要經(jīng)過以下步驟
- 若 Mark Word 無鎖泉坐,則為當(dāng)前棧幀開辟出一塊 Lock Record 內(nèi)存,并拷貝一份對(duì)象 Mark Word 信息
- 嘗試將 Mark Word 指向 Lock Record (CAS)
- 若成功裳仆,線程獲得一把輕量級(jí)鎖腕让,并將 Lock Record 指向 Mark Word
- 若失敗
- 若 Mark Word 指向 Lock Record,說明已經(jīng)擁有鎖
- 若 未指向歧斟,說明存在多線程競(jìng)爭(zhēng)纯丸,并將 Mark Word 的鎖狀態(tài) 由輕量級(jí)鎖膨脹為重量級(jí)鎖
線程釋放一把輕量級(jí)鎖時(shí),需要經(jīng)過以下步驟
- 嘗試將 Lock Record 替換 Mark Word (CAS)
- 若成功静袖,正常釋放
- 若失敗觉鼻,說明在擁有鎖的過程中,鎖已經(jīng)膨脹為重量級(jí)鎖队橙,則喚醒等待線程坠陈,通知鎖已經(jīng)被釋放
在無競(jìng)爭(zhēng)條件下,輕量級(jí)鎖無需進(jìn)行線程切換捐康,只需要鎖獲取和釋放時(shí)的兩次 CAS 操作
偏向鎖
使用輕量級(jí)鎖時(shí)仇矾,我們應(yīng)當(dāng)考慮一個(gè)問題,CAS 是很消耗 CPU 的解总,一個(gè)對(duì)象只被一個(gè)線程訪問時(shí)若未,不存在輕量級(jí)鎖競(jìng)爭(zhēng),那么輕量級(jí)鎖的兩次 CAS 操作和內(nèi)存拷貝倾鲫,就顯得些許多余粗合,所以就引入了偏向鎖
偏向鎖是指對(duì)象偏向某個(gè)線程,即在無線程競(jìng)爭(zhēng)條件下乌昔,該對(duì)象是屬于某個(gè)線程的隙疚,一旦出現(xiàn)競(jìng)爭(zhēng),應(yīng)當(dāng)撤銷偏向鎖 并升級(jí)為輕量級(jí)鎖
獲取與撤銷
線程嘗試獲取一把偏向鎖時(shí)磕道,需要經(jīng)過以下步驟
- 若 Mark Word 無鎖供屉,嘗試將 currentThreadId 復(fù)制到 Lock Record 與 Mark Word 中 (CAS)
- 若成功,線程獲得一把偏向鎖
- 若失敗,說明存在其他線程競(jìng)爭(zhēng)偏向鎖伶丐,并撤銷偏向鎖
撤銷偏向鎖時(shí)悼做,需等到安全點(diǎn) SafePoint 再執(zhí)行撤銷
加鎖策略
虛擬機(jī)的加鎖策略,是一層一層升級(jí)的哗魂,簡(jiǎn)而言之可以如下總結(jié)
- 當(dāng)競(jìng)爭(zhēng)對(duì)象無鎖時(shí)肛走,先嘗試偏向鎖
- 當(dāng)存在不止一個(gè)線程獲取偏向鎖時(shí),應(yīng)當(dāng)嘗試輕量級(jí)鎖
- 當(dāng)存在多線程競(jìng)爭(zhēng)一個(gè)輕量級(jí)鎖時(shí)录别,需考慮重量級(jí)鎖
- 當(dāng)嘗試競(jìng)爭(zhēng)重量級(jí)鎖失敗時(shí)朽色,應(yīng)先嘗試自旋鎖再考慮線程切換策略
狀態(tài)轉(zhuǎn)換
當(dāng)線程競(jìng)爭(zhēng)鎖時(shí),它們之間的狀態(tài)轉(zhuǎn)化如上圖
- Contention List:競(jìng)爭(zhēng)隊(duì)列
- Entry List:有資格成為候選人的競(jìng)爭(zhēng)隊(duì)列
- OnDeck:鎖候選人
- Owner:鎖持有者
- Wait Set:線程等待池
與 synchronized 配套使用的 wait / notify / notifyAll组题,下面我們來探討它們是如何實(shí)現(xiàn)的
- wait : 釋放鎖葫男,進(jìn)入等待池
- notify : 從等待池中挑選一名同學(xué)再次參與競(jìng)爭(zhēng)
- notifyAll : 等待池全部恢復(fù)競(jìng)爭(zhēng)
參考
- 《深入理解Java虛擬機(jī)》
- 《Java并發(fā)編程實(shí)戰(zhàn)》
- 《Java并發(fā)編程藝術(shù)》