目錄
- synchronized的使用方式
- synchronized的原理
- 線程的等待、中斷與喚醒
- 資料
- 收穫
一、synchronized的使用方式
關鍵字 synchronized可以保證在同一個時刻,只有一個線程可以執行某個方法或者某個代碼塊.有如下三種常見的使用:
- 修飾實例方法,作用於當前實例加鎖,進入同步代碼前要獲得當前實例的鎖
synchronized void syncIncrease4Obj(){
i++;
}
- 修飾靜態方法,作用於當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖
synchronized static void syncIncrease(){
i++;
}
- 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。
針對方法體可能比較大,同時存在一些比較耗時的操作,而需要同步的代碼又只有一小部分,如果直接對整個方法進行同步操作,可能會得不償失,此時我們可以使用同步代碼塊的方式對需要同步的代碼進行包裹
public void syncIncrease(){
synchronized (this){
i++;
}
}
二、 synchronized的原理
2.1 查看反彙編代碼
通過javap查看生成的class文件發現,施加了synchronized的代碼塊的實現使用了monitorenter和monitorexit指令。而同步方法則依靠修飾符ACC_SYNCHRONIZED來完成。
先看下synchronized修飾方法的反彙編
public static synchronized void increase(){
i++;
}
public synchronized void increase4Obj(){
i++;
}
---> javap -c -v Main.class 反編譯對應彙編如下:
public static synchronized void syncIncrease();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED //同步標示
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field i:I
3: iconst_1
4: iadd
5: putstatic #2 // Field i:I
8: return
LineNumberTable:
line 12: 0
line 13: 8
public synchronized void syncIncrease4Obj();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED. //同步標示
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field i:I
3: iconst_1
4: iadd
5: putstatic #2 // Field i:I
8: return
LineNumberTable:
line 19: 0
line 20: 8
ACC_SYNCHRONIZED標識,該標識指明瞭該方法是一個同步方法,JVM通過該ACC_SYNCHRONIZED訪問標誌來辨別一個方法是否聲明爲同步方法,從而執行相應的同步調用
再來看下以代碼塊方式使用synchronized 的反編譯
public void syncIncrease(){
synchronized (this){
i++;
}
}
-->javap -c Main.class 反編譯對應彙編如下:
public void syncIncrease();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter //進入同步方法
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit //退出同步方法
16: goto 24
19: astore_2
20: aload_1
21: monitorexit //退出同步方法(這個是針對異常處理的)
22: aload_2
23: athrow
24: return
Q: 從上面的字節碼可以看出,多了一個monitorexit指令,爲什麼?
A: 爲了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,異常結束時被執行的釋放monitor 的指令。
無論採用哪種方式,其本質都是對一個對象的監視器(monitor)的獲取,而這個獲取是排他的,也就是同一時刻只能有一個線程獲得synchrozied所保護對性的監視器。沒有獲得監視器的線程將會被阻塞在同步塊或者同步方法的入口處,進入BLOCKED狀態
圖片來自:《java併發編程的藝術》
當執行monitorenter指令時,當前線程將試圖獲取 objectref(即對象鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器爲 0,那線程可以成功取得 monitor,並將計數器值設置爲 1,取鎖成功。
如果當前線程已經擁有 objectref 的 monitor 的持有權,那它可以重入這個 monitor,重入時計數器的值也會加 1。
倘若其他線程已經擁有 objectref 的 monitor 的所有權,那當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行
那麼怎麼知道該線程是否獲取了某個對象的監視器吶?
下面我們一起來學習 一個對象在JVM中的的內存佈局,來尋找答案。
2.2 對象頭
在JVM中,對象在內存中的佈局分爲三塊區域:對象頭、實例數據和對齊填充。
圖片來源:深入理解Java併發之synchronized實現原理
對象頭: 是實現synchronized鎖的基礎。
實例變量:存放類的屬性數據信息,包括父類的屬性信息,如果是數組的實例部分還包括數組的長度,這部分是按照4字節對齊
填充數據:用於字節對齊。虛擬機要求對象起始地址必須是8字節的整數倍。
Java頭對象,它是synchronized的鎖對象的基礎,synchronized使用的鎖對象是存儲在Java對象頭裏的Mark Word中
圖片來自:《java併發編程的藝術》
2.3 重量級鎖、偏向鎖、輕量級鎖
2.3.1 重量級鎖
在Java1.6之前synchronized只有重量級鎖,Mark Word指針指向的是monitor對象. 在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的,其主要數據結構如下:
ObjectMonitor() {
_header = NULL;
_count = 0; //記錄個數
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //處於wait狀態的線程,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
監視器鎖(monitor)是依賴於底層的操作系統的Mutex Lock來實現的,而操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高.
Java 6之後,從JVM層面對synchronized較大優化,引入了輕量級鎖和偏向鎖,減少獲得鎖和釋放鎖所帶來的性能消耗
2.3.2 偏向鎖
偏向鎖的核心思想是,如果一個線程獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變爲偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程, 這樣就省去了大量有關鎖申請的操作,從而也就提供程序的性能
2.3.3 輕量級鎖
輕量級鎖能夠提升程序性能的依據是“對絕大部分的鎖,在整個同步週期內都不存在競爭”,注意這是經驗數據。需要了解的是,輕量級鎖所適應的場景是線程交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹爲重量級鎖。
自旋鎖
輕量級鎖失敗後,虛擬機爲了避免線程真實地在操作系統層面掛起,還會進行一項稱爲自旋鎖的優化手段。這是基於在大多數情況下,線程持有鎖的時間都不會太長,如果直接掛起操作系統層面的線程可能會得不償失,畢竟操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設在不久將來,當前的線程可以獲得鎖,因此虛擬機會讓當前想要獲取鎖的線程做幾個空循環(這也是稱爲自旋的原因),一般不會太久,可能是50個循環或100循環,在經過若干次循環後,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將線程在操作系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。最後沒辦法也就只能升級爲重量級鎖了
引用自:強烈推薦-深入理解Java併發之synchronized實現原理
鎖消除
Java虛擬機在JIT編譯時,通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間
三、 線程的等待、中斷與喚醒
3.1 等待/通知機制
等待/通知的相關方法是任意java對象都具備的,因爲這些方法被定義在Object類上
notify: 通知一個在對象上等待的線程,使其從wait方法返回,而返回的前提是該線程獲取到了對象的鎖 notify將等待隊列中的一個等待線程移動到同步隊列中,被移動的線程狀態有WAITING變成BLOCKED
notifyAll: 通知所有等待該對象上的線程。
wait: 調用該方法的線程進入WAITING狀態,並將當前線程放置到對象的等待隊列。 只有等待另一個線程通知或者被中斷纔會被返回,需要注意,調用wait方法後,會釋放對象的鎖。
wait(long): 超時等待一段時間,這裏的參數時間是毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回
Wait(long ,int) 對於超時時間更細的控制,可以達到納秒。
在使用上述幾個方法時,必須處於synchronized代碼塊或者synchronized方法中,否則就會拋出IllegalMonitorStateException異常
3.2 中斷與喚醒
//中斷線程(實例方法)
public void Thread.interrupt();
//判斷線程是否被中斷(實例方法)
public boolean Thread.isInterrupted();
//判斷是否被中斷並清除當前中斷狀態(靜態方法)
public static boolean Thread.interrupted();
當一個線程處於被阻塞狀態或者試圖執行一個阻塞操作時,使用Thread.interrupt()方式中斷該線程,注意此時將會拋出一個InterruptedException的異常,同時中斷狀態將會被複位(由中斷狀態改爲非中斷狀態)
當線程處於運行狀態時,也可調用實例方法interrupt()進行線程中斷,但同時必須手動判斷中斷狀態,並編寫中斷線程的代碼(其實就是結束run方法體的代碼)。
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(){
@Override
public void run(){
while(true){
//判斷當前線程是否被中斷
if (this.isInterrupted()){
System.out.println("線程中斷");
break;
}
}
System.out.println("已跳出循環,線程中斷!");
}
};
t1.start();
TimeUnit.SECONDS.sleep(2);
t1.interrupt();
}
結合上面兩點,可以採用如何方式判斷:
public void run(){
try {
//判斷當前線程是否已中斷,注意interrupted方法是靜態的,執行後會對中斷狀態進行復位
while (!Thread.interrupted()) {
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
}
}
- 線程的中斷操作對於正在等待獲取的鎖對象的synchronized方法或者代碼塊並不起作用. 由於對象鎖被其他線程佔用,導致等待線程只能等到鎖,此時我們調用了thread.interrupt();但並不能中斷線程。
資料
- 圖書:《java併發編程的藝術》
- 強烈推薦-深入理解Java併發之synchronized實現原理
收穫
通過本篇的學習實踐
- 回顧了synchronized的基本使用
- 學習了synchronized的實現同步的原理
- 瞭解了JVM對synchronized做的優化
- 學習回顧了線程的等待、中斷與喚醒
感謝你的閱讀
下一篇我們繼續學習實踐Java併發編程系列-Lock相關,歡迎關注公衆號“音視頻開發之旅”,一起學習成長。
歡迎交流