線程池(二):深入剖析synchronized關鍵字的底層原理
- 線程池(二):深入剖析`synchronized`關鍵字的底層原理
- 一、基本使用
- 1.1 修飾實例方法
- 1.2 修飾靜態方法
- 1.3 修飾代碼塊
- 二、Monitor
- 2.1 Monitor的概念
- 2.2 Monitor的實現原理
- 2.3 Monitor與`synchronized`的關系
- 三、synchronized關鍵字的底層原理 - 進階
- 3.1 對象的內存結構
- 3.2 MarkWord
- 3.3 再說Monitor重量級鎖
- 3.4 輕量級鎖
- 3.5 偏向鎖
- 3.6 談談JMM(Java內存模型)
線程池(二):深入剖析synchronized
關鍵字的底層原理
一、基本使用
1.1 修飾實例方法
當synchronized
修飾一個實例方法時,它鎖定的是當前對象(this
)。例如:
public class SynchronizedExample {public synchronized void synchronizedMethod() {// 同步代碼塊// 同一時刻,只有一個線程能進入這個方法}
}
在上述代碼中,任何線程在調用synchronizedMethod
方法時,都需要獲取當前對象的鎖。如果一個線程已經持有了這個鎖,其他線程就需要等待,直到該線程釋放鎖。
1.2 修飾靜態方法
當synchronized
修飾靜態方法時,它鎖定的是當前類的Class
對象。因為靜態方法屬于類,而不是某個具體的實例。示例如下:
public class StaticSynchronizedExample {public static synchronized void staticSynchronizedMethod() {// 同步代碼塊// 同一時刻,只有一個線程能進入這個靜態方法}
}
不管有多少個該類的實例,對于這個靜態同步方法,同一時刻只有一個線程可以執行。這是因為所有線程共享類的Class
對象,鎖的就是這個唯一的Class
對象。
1.3 修飾代碼塊
synchronized
還可以修飾代碼塊,這種方式更加靈活,可以指定具體要鎖定的對象。例如:
public class SynchronizedBlockExample {private final Object lock = new Object();public void someMethod() {synchronized (lock) {// 同步代碼塊// 同一時刻,只有一個線程能進入這個代碼塊}}
}
這里通過synchronized (lock)
指定了鎖定的對象是lock
。當多個線程同時訪問someMethod
方法時,只有一個線程能獲取到lock
對象的鎖并執行同步代碼塊中的內容。
二、Monitor
2.1 Monitor的概念
Monitor(監視器)是Java并發編程中實現同步的一個核心概念。它可以理解為一個同步工具,也可以說是一種同步機制。每個Java對象都可以關聯一個Monitor。當一個線程想要進入同步代碼塊(無論是synchronized
修飾的方法還是代碼塊)時,它需要先獲取對應的Monitor。
2.2 Monitor的實現原理
在HotSpot虛擬機中,Monitor是由ObjectMonitor
結構體實現的。它主要包含以下幾個關鍵部分:
- header:對象頭,用于存儲對象的一些元數據信息,比如對象的哈希碼、對象的分代年齡等。
- count:記錄該Monitor被獲取的次數。當一個線程成功獲取到Monitor后,
_count
會加1,每次釋放鎖時,_count
會減1。當_count
為0時,代表該Monitor沒有被任何線程持有。 - owner:指向當前持有該Monitor的線程。如果當前沒有線程持有該Monitor,
_owner
為null
。 - WaitSet:等待隊列,當線程調用對象的
wait()
方法時,該線程會被放入這個等待隊列中,進入等待狀態。 - EntryList:入口隊列,當多個線程同時競爭一個Monitor時,沒有獲取到鎖的線程會被放入這個入口隊列中等待。
2.3 Monitor與synchronized
的關系
synchronized
關鍵字的底層實現依賴于Monitor。當一個線程進入synchronized
修飾的同步代碼塊或方法時,實際上就是去獲取對應的Monitor。如果獲取成功,就可以執行同步代碼;如果獲取失敗,就會被放入EntryList
隊列中等待。當持有鎖的線程執行完同步代碼或者調用wait()
方法時,會釋放Monitor,此時會從EntryList
隊列中喚醒一個等待的線程來獲取Monitor。
三、synchronized關鍵字的底層原理 - 進階
3.1 對象的內存結構
在Java中,對象在內存中的布局主要包括三個部分:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
- 對象頭(Header):對象頭又分為兩部分,一部分是用于存儲對象自身的運行時數據,比如哈希碼(HashCode)、對象的分代年齡、鎖標志位等,這部分數據被稱為
MarkWord
;另一部分是指向對象所屬類的Class
對象的指針,用于確定對象的類型。 - 實例數據(Instance Data):這部分用于存儲對象的成員變量,包括從父類繼承下來的成員變量和本類定義的成員變量。
- 對齊填充(Padding):由于虛擬機要求對象的起始地址必須是8字節的整數倍,所以當對象頭和實例數據部分的總大小不是8字節的整數倍時,需要通過對齊填充來補足。
3.2 MarkWord
MarkWord
是對象頭中非常重要的一部分,它在不同的鎖狀態下會存儲不同的信息。在32位虛擬機中,MarkWord
的長度是32位(4個字節),在64位虛擬機中,MarkWord
的長度是64位(8個字節)。以下是不同鎖狀態下MarkWord
的存儲內容:
- 無鎖狀態:在無鎖狀態下,
MarkWord
存儲對象的哈希碼、對象的分代年齡等信息。例如在32位虛擬機中,前25位存儲對象的哈希碼,后4位存儲對象的分代年齡,最后3位是鎖標志位(01表示無鎖)。 - 偏向鎖狀態:當對象進入偏向鎖狀態時,
MarkWord
中會存儲持有該鎖的線程ID等信息。 - 輕量級鎖狀態:在輕量級鎖狀態下,
MarkWord
會存儲指向棧幀中鎖記錄的指針。 - 重量級鎖狀態:當對象處于重量級鎖狀態時,
MarkWord
會存儲指向Monitor對象的指針。
3.3 再說Monitor重量級鎖
當多個線程競爭同一個鎖,且競爭比較激烈時,輕量級鎖會升級為重量級鎖。此時,MarkWord
中存儲的是指向ObjectMonitor
的指針。重量級鎖是通過操作系統的互斥量(Mutex)來實現的,線程獲取和釋放鎖都需要進行用戶態和內核態的切換,這種切換開銷比較大。
當一個線程進入synchronized
同步代碼塊時,如果發現是重量級鎖,它會進入ObjectMonitor
的EntryList
隊列中等待。持有鎖的線程執行完同步代碼后,會釋放鎖,然后從EntryList
隊列中喚醒一個等待的線程。被喚醒的線程會再次嘗試獲取鎖,獲取成功后才能執行同步代碼。
3.4 輕量級鎖
輕量級鎖是為了在沒有多線程競爭或者競爭不激烈的情況下,減少獲取鎖和釋放鎖的開銷而引入的。當一個線程進入synchronized
同步代碼塊時,會在當前線程的棧幀中創建一個鎖記錄(Lock Record),并將MarkWord
復制到鎖記錄中。然后,線程嘗試通過CAS(Compare and Swap,比較并交換)操作將MarkWord
更新為指向鎖記錄的指針。如果CAS操作成功,說明該線程獲取到了輕量級鎖,就可以執行同步代碼。
如果CAS操作失敗,說明有其他線程已經持有了該鎖,此時輕量級鎖會嘗試自旋(Spin)一定次數來等待鎖的釋放。自旋是指線程不放棄CPU的執行權,在原地等待一段時間,希望持有鎖的線程能盡快釋放鎖。如果自旋一定次數后仍然沒有獲取到鎖,輕量級鎖就會升級為重量級鎖。
3.5 偏向鎖
偏向鎖是在JDK 6中引入的,它的目的是為了在只有一個線程訪問同步代碼塊的情況下,進一步減少獲取鎖的開銷。當一個線程訪問synchronized
同步代碼塊時,會檢查MarkWord
中是否已經記錄了該線程的ID。如果已經記錄,說明該線程已經持有了偏向鎖,直接進入同步代碼塊執行。
如果MarkWord
中沒有記錄該線程的ID,會通過CAS操作將線程ID記錄到MarkWord
中。如果CAS操作成功,就表示該線程獲取到了偏向鎖。當有其他線程嘗試獲取該鎖時,偏向鎖會被撤銷,升級為輕量級鎖。
3.6 談談JMM(Java內存模型)
Java內存模型(JMM)定義了Java程序中多線程訪問共享變量的規則。它規定了一個線程如何和何時可以看到由其他線程修改過后的共享變量的值,以及在必須時如何同步的訪問共享變量。
在synchronized
關鍵字的實現中,JMM起到了重要的作用。當一個線程獲取到鎖進入synchronized
同步代碼塊時,會從主內存中讀取共享變量的值到工作內存中。在同步代碼塊執行過程中,對共享變量的修改會先在工作內存中進行。當線程執行完同步代碼塊釋放鎖時,會將工作內存中修改后的共享變量的值寫回到主內存中。這樣可以保證在同一時刻,只有一個線程能夠對共享變量進行修改,并且其他線程能夠看到最新的修改結果,從而保證了多線程環境下共享變量的可見性和一致性。
通過對synchronized
關鍵字從基本使用到深入底層原理的剖析,包括Monitor機制、對象內存結構、不同鎖狀態以及與Java內存模型的關系等方面,我們對synchronized
在Java并發編程中的作用和實現有了一個全面而深入的理解。這有助于我們在實際開發中更合理、高效地使用synchronized
來解決多線程同步問題。