一、synchronized的簡(jiǎn)單介紹
關(guān)鍵字 synchronized可以保證在同一個(gè)時(shí)刻,只有一個(gè)線程可以執(zhí)行某個(gè)方法或者某個(gè)代碼塊(主要是對(duì)方法或者代碼塊中存在共享數(shù)據(jù)的操作),同時(shí)我們還應(yīng)該注意到synchronized另外一個(gè)重要的作用,synchronized可保證一個(gè)線程的變化(主要是共享數(shù)據(jù)的變化)被其他線程所看到(保證可見性,完全可以替代volatile功能)。簡(jiǎn)單來說概括就是三個(gè)特性:
- 原子性:確保線程互斥的訪問同步代碼;
- 可見性:可保證一個(gè)線程的變化(主要是共享數(shù)據(jù)的變化)被其他線程所看到
- 有序性:有效解決重排序問題,即 “一個(gè)unlock操作先行發(fā)生(happen-before)于后面對(duì)同一個(gè)鎖的lock操作”
二、synchronized應(yīng)用
1.synchronized使用場(chǎng)景
- 修飾實(shí)例方法,作用于當(dāng)前實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖
- 修飾靜態(tài)方法,作用于當(dāng)前類對(duì)象加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類對(duì)象的鎖
- 修飾代碼塊,指定加鎖對(duì)象,對(duì)給定對(duì)象加鎖,進(jìn)入同步代碼庫(kù)前要獲得給定對(duì)象的鎖。
2.synchronized使用的注意事項(xiàng):
- 若是對(duì)象鎖,則每個(gè)對(duì)象都持有一把自己的獨(dú)一無二的鎖,且對(duì)象之間的鎖互不影響 。若是類鎖,所有該類的對(duì)象共用這把鎖。
- 一個(gè)線程獲取一把鎖,沒有得到鎖的線程只能排隊(duì)等待;
- synchronized 是可重入鎖,避免很多情況下的死鎖發(fā)生。
- synchronized 方法若發(fā)生異常,則JVM會(huì)自動(dòng)釋放鎖。
- 鎖對(duì)象不能為空,否則拋出NPE(NullPointerException)
- synchronized 本身是不具備繼承性的:即父類的synchronized 方法,子類重寫該方法,分情況討論:沒有synchonized修飾,則該子類方法不是線程同步的。
- synchronized本身修飾的范圍越小越好。畢竟是同步阻塞。
3.synchronized的常見問題
同時(shí)訪問synchronized的靜態(tài)和非靜態(tài)方法,能保證線程安全嗎?
不能,兩者的鎖對(duì)象不一樣。前者是類鎖(XXX.class),后者是this同時(shí)訪問synchronized方法和非同步方法,能保證線程安全嗎?
結(jié)論:不能,因?yàn)閟ynchronized只會(huì)對(duì)被修飾的方法起作用。兩個(gè)線程同時(shí)訪問兩個(gè)對(duì)象的非靜態(tài)同步方法能保證線程安全嗎?
結(jié)論:不能,每個(gè)對(duì)象都擁有一把鎖。兩個(gè)對(duì)象相當(dāng)于有兩把鎖,導(dǎo)致鎖對(duì)象不一致。(PS:如果是類鎖,則所有對(duì)象共用一把鎖)若synchronized方法拋出異常,會(huì)導(dǎo)致死鎖嗎?
JVM會(huì)自動(dòng)釋放鎖,不會(huì)導(dǎo)致死鎖問題若synchronized的鎖對(duì)象能為空嗎?會(huì)出現(xiàn)什么情況?
鎖對(duì)象不能為空,否則拋出NPE(NullPointerException)若synchronized的鎖對(duì)象能為空嗎?會(huì)出現(xiàn)什么情況?
鎖對(duì)象不能為空,否則拋出NPE(NullPointerException)
三、synchronized原理
Java 虛擬機(jī)中的同步(Synchronization)基于進(jìn)入和退出管程(Monitor)對(duì)象實(shí)現(xiàn)。同步方法并不是由 monitorenter 和 monitorexit 指令來實(shí)現(xiàn)同步的,而是由方法調(diào)用指令讀取運(yùn)行時(shí)常量池中方法的 ACC_SYNCHRONIZED 標(biāo)志來隱式實(shí)現(xiàn)的。
1、關(guān)于Java對(duì)象頭與Monitor
對(duì)象在內(nèi)存中的布局分為三塊區(qū)域:1、對(duì)象頭、2、實(shí)例數(shù)據(jù)和3、對(duì)齊填充。
- 實(shí)例變量:存放類的屬性數(shù)據(jù)信息
- 填充數(shù)據(jù):由于虛擬機(jī)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對(duì)齊。
- Java頭對(duì)象:Mark Word 和 Class Metadata Address 組成
| 頭對(duì)象結(jié)構(gòu) | 說明 |
|---|---|
| Mark Word | 存儲(chǔ)對(duì)象的hashCode、鎖信息或分代年齡或GC標(biāo)志等信息 |
| Class Metadata Address | 類型指針指向?qū)ο蟮念愒獢?shù)據(jù),JVM通過這個(gè)指針確定該對(duì)象是哪個(gè)類的實(shí)例。 |
Mark Word 被設(shè)計(jì)成為一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu),默認(rèn)的存儲(chǔ)結(jié)構(gòu)如下:
| 鎖狀態(tài) | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit 鎖標(biāo)志位 |
|---|---|---|---|---|
| 無鎖狀態(tài) | 對(duì)象HashCode | 對(duì)象分代年齡 | 0 | 01 |
Mark Word會(huì)隨著程序的運(yùn)行發(fā)生變化,可能變化為存儲(chǔ)以下4種數(shù)據(jù):

由synchronized的對(duì)象鎖,指針指向的是monitor對(duì)象(也稱為管程或監(jiān)視器鎖)的地址,所以每個(gè)對(duì)象都存在著一個(gè) monitor 與之關(guān)聯(lián),monitor是由ObjectMonitor實(shí)現(xiàn)的(C++實(shí)現(xiàn)的),源碼如下:
ObjectMonitor() {
... //其他忽略,核心為如下三個(gè)
_count = 0; //記錄個(gè)數(shù)
_WaitSet = NULL; //處于wait狀態(tài)的線程,會(huì)被加入到_WaitSet
_EntryList = NULL ; //處于等待鎖block狀態(tài)的線程,會(huì)被加入到該列表
}
可以看到ObjectMonitor中有兩個(gè)隊(duì)列,_WaitSet 和 _EntryList 用來保存ObjectWaiter對(duì)象列表(每個(gè)等待鎖的線程都會(huì)被封裝成ObjectWaiter對(duì)象),其工作流程大致如下:
- 當(dāng)多個(gè)線程同時(shí)訪問一段同步代碼時(shí),首先會(huì)進(jìn)入 _EntryList 集合。
- 當(dāng)線程獲取到對(duì)象的monitor 后進(jìn)入 _Owner 區(qū)域,并把monitor中的owner變量設(shè)置為當(dāng)前線程,同時(shí)monitor中的計(jì)數(shù)器count加1
- 若線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的monitor,owner變量恢復(fù)為null,count自減1,同時(shí)該線程進(jìn)入 WaitSe t集合中等待被喚醒。
- 若當(dāng)前線程執(zhí)行完畢也將釋放monitor(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor(鎖)。

monitor對(duì)象存在于每個(gè)Java對(duì)象的對(duì)象頭中(存儲(chǔ)的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對(duì)象可以作為鎖的原因
2、synchronized字節(jié)碼語義
將synchronized修飾的同步代碼塊利用javap反編譯后得到字節(jié)碼如下:

我們主要需要關(guān)注如下:
3: monitorenter //進(jìn)入同步方法
//..........省略其他
13: monitorexit //退出同步方法
14: goto 22
//省略其他.......
19: monitorexit //退出同步方法
monitorenter指令,線程嘗試獲取monitor的所有權(quán),過程如下:
- 如果monitor的進(jìn)入數(shù)為0,則該線程進(jìn)入monitor,然后將進(jìn)入數(shù)設(shè)置為1,該線程即為monitor的所有者;
- 如果線程已經(jīng)占有該monitor,只是重新進(jìn)入,則進(jìn)入monitor的進(jìn)入數(shù)加1;
- 如果其他線程已經(jīng)占用了monitor,則該線程進(jìn)入阻塞狀態(tài),直到monitor的進(jìn)入數(shù)為0,再重新嘗試獲取monitor的所有權(quán);
monitorexit指令,線程執(zhí)行完畢釋放鎖,過程如下:
- monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0,那線程退出monitor,不再是這個(gè)monitor的所有者。
- monitorexit指令出現(xiàn)了兩次,第1次為同步正常退出釋放鎖;第2次為發(fā)生異步退出釋放鎖;
3、synchronized方法的底層原理
上面講的是同步代碼塊的方式,方法級(jí)的同步是隱式,無需通過字節(jié)碼指令來控制的,它實(shí)現(xiàn)在方法調(diào)用和返回操作之中。JVM可以從方法常量池中的方法表結(jié)構(gòu)(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標(biāo)志區(qū)分一個(gè)方法是否同步方法。
- 當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì) 檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先持有monitor, 然后再執(zhí)行方法,最后在方法完成( 無論是正常完成還是非正常完成 )時(shí)釋放monitor。
- 在方法執(zhí)行期間,執(zhí)行線程持有了monitor,其他任何線程都無法再獲得同一個(gè)monitor。如果一個(gè)同步方法執(zhí)行期間拋 出了異常,并且在方法內(nèi)部無法處理此異常,那這個(gè)同步方法所持有的monitor將在異常拋到同步方法之外時(shí)自動(dòng)釋放。
synchronized修飾的方法并沒有monitorenter指令和monitorexit指令,取得代之的確實(shí)是ACC_SYNCHRONIZED標(biāo)識(shí),該標(biāo)識(shí)指明了該方法是一個(gè)同步方法,JVM通過該ACC_SYNCHRONIZED訪問標(biāo)志來辨別一個(gè)方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。這便是synchronized鎖在同步代碼塊和同步方法上實(shí)現(xiàn)的基本原理。
//省略沒必要的字節(jié)碼
public synchronized void syncTask();
descriptor: ()V
//方法標(biāo)識(shí)ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 12: 0
line 13: 10
}
四、synchronized的改進(jìn)與優(yōu)化
Java早期版本中,synchronized屬于重量級(jí)鎖,效率低下,因?yàn)楸O(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的Mutex Lock來實(shí)現(xiàn)的,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,時(shí)間成本相對(duì)較高,這也是為什么早期的synchronized效率低的原因。
在Java 6之后Java官方對(duì)從JVM層面對(duì)synchronized較大優(yōu)化,引入了輕量級(jí)鎖和偏向鎖,鎖的狀態(tài)總共有四種,無鎖狀態(tài)、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。隨著鎖的競(jìng)爭(zhēng),鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)的重量級(jí)鎖。由輕到重的順序就是:偏向鎖-->輕量級(jí)鎖-->重量級(jí)鎖。 JDK 1.6 中默認(rèn)是開啟偏向鎖和輕量級(jí)鎖的。
1.偏向鎖
適用于:不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得。
偏向鎖的核心思想:如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式,此時(shí)Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)這個(gè)線程再次請(qǐng)求鎖時(shí),無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作,從而也就提高程序的性能。對(duì)于沒有鎖競(jìng)爭(zhēng)的場(chǎng)合,偏向鎖有很好的優(yōu)化效果。
2.輕量級(jí)鎖
適用于:當(dāng)鎖競(jìng)爭(zhēng)升級(jí)了后,有可能每次申請(qǐng)鎖的線程都是不相同的,但時(shí)線程交替執(zhí)行同步塊的場(chǎng)合,,此時(shí)Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級(jí)鎖的結(jié)構(gòu)。輕量級(jí)鎖的思想是依賴經(jīng)驗(yàn)情況,對(duì)絕大部分的鎖,在整個(gè)同步周期內(nèi)都不存在競(jìng)爭(zhēng)。
3.重量級(jí)鎖
輕量級(jí)鎖失敗后,虛擬機(jī)為了避免線程在操作系統(tǒng)層面掛起,還會(huì)進(jìn)行一項(xiàng)稱為自旋鎖的優(yōu)化手段。這是基于在大多數(shù)情況下,線程持有鎖的時(shí)間都不會(huì)太長(zhǎng),如果直接掛起操作系統(tǒng)層面的線程可能會(huì)得不償失,畢竟操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,時(shí)間成本相對(duì)較高,因此自旋鎖會(huì)假設(shè)在不久將來,當(dāng)前的線程可以獲得鎖,因此虛擬機(jī)會(huì)讓當(dāng)前想要獲取鎖的線程做幾個(gè)空循環(huán)(這也是稱為自旋的原因),一般不會(huì)太久,可能是50個(gè)循環(huán)或100循環(huán),在經(jīng)過若干次循環(huán)后,如果得到鎖,就順利進(jìn)入臨界區(qū)。如果還不能獲得鎖,那就會(huì)將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式,這種方式確實(shí)也是可以提升效率的。最后沒辦法也就只能升級(jí)為重量級(jí)鎖了。
4.鎖消除
消除鎖是虛擬機(jī)另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機(jī)在JIT編譯時(shí)(可以簡(jiǎn)單理解為當(dāng)某段代碼即將第一次被執(zhí)行時(shí)進(jìn)行編譯,又稱即時(shí)編譯),通過對(duì)運(yùn)行上下文的掃描,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖,通過這種方式消除沒有必要的鎖,可以節(jié)省毫無意義的請(qǐng)求鎖時(shí)間,如下StringBuffer的append是一個(gè)同步方法,但是在add方法中的StringBuffer屬于一個(gè)局部變量,并且不會(huì)被其他線程所使用,因此StringBuffer不可能存在共享資源競(jìng)爭(zhēng)的情景,JVM會(huì)自動(dòng)將其鎖消除。
public class StringBufferRemoveSync {
public void add(String str1, String str2) {
//StringBuffer是線程安全,由于sb只會(huì)在append方法中使用,不可能被其他線程引用
//因此sb屬于不可能共享的資源,JVM會(huì)自動(dòng)消除內(nèi)部的鎖
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
for (int i = 0; i < 10000000; i++) {
rmsync.add("abc", "123");
}
}
}
syncchronized的深度思考
1、面試官:為什么synchronized無法禁止指令重排,卻能保證有序性?
首先,最好的解決有序性問題的辦法,就是禁止處理器優(yōu)化和指令重排,就像volatile中使用內(nèi)存屏障一樣。但是synchronized沒有使用內(nèi)存屏障。
在synchronized這邊,加了鎖之后,只能有一個(gè)線程獲得到了鎖,獲得不到鎖的線程就要阻塞。所以同一時(shí)間只有一個(gè)線程執(zhí)行,相當(dāng)于單線程,而單線程的指令重排是沒有問題的。在Java中,不管怎么排序,都不能影響單線程程序的執(zhí)行結(jié)果。這就是as-if-serial語義,所有硬件優(yōu)化的前提都是必須遵守as-if-serial語義(as-if-serial語義的意思是:不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能被改變)。
因?yàn)橛衋s-if-serial語義保證,單線程的有序性就天然存在了。
2、既然synchronized是"萬能"的,為什么還需要volatile呢?
這個(gè)是針對(duì)DSL的單例模式來談的,我們知道對(duì)singleton使用volatile約束,保證他的初始化過程不會(huì)被指令重排。但是synchronized是無法禁止指令重排和處理器優(yōu)化的。也就是只看Thread1的話,因?yàn)榫幾g器會(huì)遵守as-if-serial語義,所以這種優(yōu)化不會(huì)有任何問題,對(duì)于這個(gè)線程的執(zhí)行結(jié)果也不會(huì)有任何影響。但是Thread1內(nèi)部的指令重排卻對(duì)Thread2產(chǎn)生了影響。
我們可以說,synchronized保證的有序性是多個(gè)線程之間的有序性,即被加鎖的內(nèi)容要按照順序被多個(gè)線程執(zhí)行。但是其內(nèi)部的同步代碼還是會(huì)發(fā)生重排序,只不過由于編譯器和處理器都遵循as-if-serial語義,所以我們可以認(rèn)為這些重排序在單線程內(nèi)部可忽略。
參考引用
1、深入理解Java并發(fā)之synchronized實(shí)現(xiàn)原理
2、☆啃碎并發(fā)(七):深入分析Synchronized原理