自顶向下彻底理解 Java 中的 Synchronized
閱讀本文至少要知道 synchronized 用來是干什么的... 需要的前置知識(shí)還有 Java 對(duì)象頭和 Java 字節(jié)碼的部分知識(shí)。
synchronized 的使用
synchronized 有三種使用方式,三種方式鎖住的對(duì)象是不相同的。
鎖分為實(shí)例對(duì)象鎖和 class 對(duì)象鎖 和 類對(duì)象鎖,注意這三種鎖是不一樣的。
- 修飾實(shí)例方法,此時(shí)鎖住的是對(duì)象,鎖分為實(shí)例對(duì)象鎖
- 修飾靜態(tài)方法,此時(shí)鎖住的是類對(duì)象鎖
- 修飾代碼段,此時(shí)鎖住的是括號(hào)中的對(duì)象(synchronized(this)),可以是實(shí)例對(duì)象鎖或者 class 對(duì)象鎖(synchronized(Object.class))
此時(shí)出現(xiàn)了鎖住類和鎖住對(duì)象,要注意這兩個(gè)鎖是不同的,在一個(gè)線程拿到類的鎖時(shí),另外一個(gè)線程是可以拿到對(duì)象的鎖的。
synchronized 底層語義實(shí)現(xiàn)
每個(gè)對(duì)象都存在著一個(gè) monitor 與之關(guān)聯(lián),對(duì)象與其 monitor 之間的關(guān)系有存在多種實(shí)現(xiàn)方式,如 monitor 可以與對(duì)象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對(duì)象鎖時(shí)自動(dòng)生成,但當(dāng)一個(gè) monitor 被某個(gè)線程持有后,它便處于鎖定狀態(tài)。
當(dāng)多個(gè)線程同時(shí)請(qǐng)求某個(gè)對(duì)象監(jiān)視器時(shí),新請(qǐng)求鎖的線程將首先被加入到 ConetentionList 中。對(duì)象監(jiān)視器會(huì)設(shè)置幾種狀態(tài)用來區(qū)分請(qǐng)求的線程:
Contention List:所有請(qǐng)求鎖的線程將被首先放置到該競爭隊(duì)列
Entry List:Contention List 中那些有資格成為候選人的線程被移到Entry List
Wait Set:那些調(diào)用 wait 方法被阻塞的線程被放置到 Wait Set
OnDeck:任何時(shí)刻最多只能有一個(gè)線程正在競爭鎖,該線程稱為 OnDeck
Owner:獲得鎖的線程稱為 Owner
!Owner:釋放鎖的線程
代碼同步塊和方法級(jí)別的 synchronized 使用在JVM 層實(shí)現(xiàn)是不一樣的。
synchrionized 在代碼同步塊的入口插入 monitorenter,在同步塊出口插入monitorexit 來實(shí)現(xiàn)互斥,可以通過反編譯看到。
方法級(jí)別的同步是隱式的,在字節(jié)碼層面上沒有顯示出來。JVM 可以從方法常量池中的方法表結(jié)構(gòu)中的 ACC_SYNCHRONIZED 字段訪問標(biāo)志區(qū)分一個(gè)方法是否為同步方法。如果是同步方法,則去獲取 monitor,然后執(zhí)行方法。
具體可以查看《深入理解 JVM 虛擬機(jī)》 中字節(jié)碼一章。
對(duì) synchronized 的優(yōu)化
JDK 1.6 實(shí)現(xiàn)了對(duì)鎖的大量優(yōu)化。可以分為兩種,一種是減少對(duì) synchronized 的使用,一種是在特殊條件下使用更輕量級(jí)的鎖來代替 synchronized。
減少對(duì)鎖的使用
鎖消除
當(dāng)編譯器檢測到一些被加上 synchronized 的代碼不存在競爭的時(shí)候(通過逃逸分析,感興趣可以去看一下《深入理解 Java 虛擬機(jī)》),就會(huì)被視為線程私有的,鎖會(huì)被安全的消除掉。
鎖粗化
當(dāng)編譯器發(fā)現(xiàn) synchronized 被加入在循環(huán)當(dāng)中,不斷的加鎖解鎖會(huì)有極大的效率問題。不要認(rèn)為你不會(huì)寫出這么傻的代碼,JDK 中有許多方法是同步的,比如 HashTable 中的一些方法。
for (int i = 0; i < 100; i++) {synchronized (this) {//do something} }編譯器會(huì)自動(dòng)把它優(yōu)化成
synchronized (this) {for (int i = 0; i < 100; i++) {//do something} }來減少鎖的獲取和釋放。
自旋鎖與自適應(yīng)鎖
有相當(dāng)多一段代碼在代碼同步塊中只運(yùn)行一小會(huì)兒,如果為了等待這一會(huì)兒去掛起和恢復(fù)線程,切換線程帶來的開銷不是很值得,在引入了自旋鎖后,當(dāng)遇到鎖被別的線程占用的時(shí)候,這個(gè)線程就進(jìn)入一段忙循環(huán),這就是自旋。
但是如果多次忙循環(huán)后仍然獲取不到鎖,那么只能掛起線程將鎖升級(jí)為重量級(jí)鎖了。
自適應(yīng)鎖會(huì)記錄之前在代碼同步快的運(yùn)行時(shí)間來決定是否要執(zhí)行自旋以及自旋的時(shí)間,如果之前自旋成功過,那么這次也很有可能會(huì)自旋成功。如果之前自旋失敗,那么就省略掉自旋過程直接掛起線程避免浪費(fèi) CPU 資源。
通過輕量級(jí)鎖來代替 synchronized
輕量級(jí)鎖設(shè)計(jì)出來是想要在競爭較少的情況下減少 synchronized 的性能消耗,而不是用來代替 synchronized 的。想要看懂輕量級(jí)鎖的使用需要對(duì) Java 對(duì)象頭有一定的了解。關(guān)于 Java 對(duì)象頭可以參考。好,接下來我就默認(rèn)認(rèn)為你懂 Mark Word 是什么了。
鎖的膨脹過程是 偏向鎖→輕量級(jí)鎖→重量級(jí)鎖,膨脹過程的單方向的。不能縮小回來。
下面是 Mark Word 的內(nèi)容和鎖的關(guān)系。
| 對(duì)象哈希碼,對(duì)象分代年齡 | 01 | 未鎖定 |
| 指向記錄鎖指針 | 00 | 輕量級(jí)鎖定 |
| 指向重量級(jí)鎖指針 | 10 | 膨脹(重量級(jí)鎖定) |
| 空 | 11 | GC 標(biāo)記 |
| 偏向線程 id,時(shí)間戳,分代年齡 | 01 | 可偏向 |
偏向鎖
偏向鎖的思想就是:鎖經(jīng)常被同一個(gè)線程重復(fù)獲取,那么可以通過設(shè)置偏向鎖來避免使用重量級(jí)鎖。因?yàn)槿绻@段時(shí)間只有這一個(gè)線程在重復(fù)獲取這個(gè)對(duì)象的鎖,那么對(duì)這部分代碼的同步就是無意義的。
當(dāng)線程獲取鎖的時(shí)候發(fā)現(xiàn) Mark Word 是未鎖定的狀態(tài),那么就采用 CAS 把這個(gè) Mark Word 設(shè)置成偏向狀態(tài),把這個(gè)線程的 id 設(shè)置進(jìn)去,然后如果這個(gè)線程再次獲取這個(gè)鎖的時(shí)候發(fā)現(xiàn)這個(gè)偏向鎖的 id 和當(dāng)前線程的 id 一樣則不需要同步直接運(yùn)行。
當(dāng)有另外一個(gè)線程嘗試獲取這個(gè)偏向鎖的時(shí)候,鎖會(huì)恢復(fù)到未鎖定或者輕量級(jí)鎖的狀態(tài)。
- 如果對(duì)象未被鎖定,則會(huì)變成未鎖定的,不可偏向的對(duì)象
- 如果對(duì)象被鎖定了,則會(huì)變成輕量級(jí)鎖狀態(tài)
如果大多數(shù)鎖總是被多個(gè)不同的線程訪問,那么偏向模式就是多余的,可以 采用 --XX:UseBiaseLocking 來禁止偏向鎖來提高性能。
輕量級(jí)鎖
當(dāng)線程進(jìn)入一個(gè)代碼同步塊的時(shí)候,虛擬機(jī)將使用 CAS 將 Mark Word 更新為指向 Lock Record 的指針。如果成功則線程擁有這個(gè)對(duì)象鎖,mark word 將被設(shè)為 00。
如果更新失敗,則檢查該線程是否持有這個(gè)對(duì)象鎖,如果已經(jīng)持有則直接向下執(zhí)行
如果沒有持有這個(gè)對(duì)象鎖則輕量級(jí)鎖膨脹為重量級(jí)鎖,鎖標(biāo)志狀態(tài)變?yōu)?10。
參考文獻(xiàn)
- 周志明. 深入理解 Java 虛擬機(jī) [M]. 機(jī)械工業(yè)出版社, 2011.
- 方騰飛.Java 并發(fā)編程的藝術(shù) [M]. 機(jī)械工業(yè)出版社, 2015.
- 深入理解Java并發(fā)之synchronized實(shí)現(xiàn)原理
- [JVM底層又是如何實(shí)現(xiàn)synchronized的
轉(zhuǎn)載于:https://www.cnblogs.com/zjmeow/p/9818739.html
總結(jié)
以上是生活随笔為你收集整理的自顶向下彻底理解 Java 中的 Synchronized的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: BZOJ2208 [Jsoi20
- 下一篇: 学习目标