文章目錄
- 一、 synchronized 關鍵字
- 二、Java對象結構
- 1. 對象頭
- 2. 對象體
- 3. 對齊字節
- 4. 對象頭中的字段長度
- 5. Mark Word 的結構信息
- 6. 使用 JOL 工具查看對象的布局
- 三、Java 內置鎖機制
- 3.1 內置鎖的演進過程
- 1. 無鎖狀態
- 2. 偏向鎖狀態
- 3. 輕量級鎖狀態
- 4. 重量級鎖狀態
一、 synchronized 關鍵字
在Java中,synchronized
關鍵字通過內置鎖(也稱為監視器鎖或互斥鎖)機制實現線程間的互斥訪問。具體來說,它確保在同一時刻只有一個線程能夠執行特定的同步代碼塊或方法。
Java內置鎖
每個對象都有一個與之關聯的內置鎖(
monitor lock
)。當一個線程進入synchronized
方法或代碼塊時,它會嘗試獲取該對象的內置鎖。如果成功獲取到鎖,則該線程可以繼續執行;否則,線程將被阻塞并放入等待隊列中,直到鎖被釋放。
鎖的狀態
無鎖狀態:對象處于未鎖定狀態,任何線程都可以嘗試獲取該對象的鎖。
偏向鎖狀態:JVM優化的一種方式,在幾乎沒有競爭的情況下,鎖會偏向于第一個獲取它的線程,減少不必要的CAS操作。
輕量級鎖狀態:當有多個線程嘗試獲取同一個鎖但競爭不激烈時,JVM會使用CAS操作來避免重量級鎖帶來的開銷。
重量級鎖狀態:在高競爭情況下,鎖會膨脹為重量級鎖,涉及操作系統級別的線程掛起和恢復,帶來較高的性能開銷。
synchronized
的使用方式有:
1.
synchronized
修飾實例方法(非靜態方法)
當一個實例方法被聲明為synchronized
時,它會鎖定調用該方法的對象(即當前對象,this)。這意味著同一時間只能有一個線程執行這個實例方法。
public class SynchronizedExample {public synchronized void synchronizedInstanceMethod() {// 同步代碼塊System.out.println(Thread.currentThread().getName() + " is executing synchronized instance method.");try {Thread.sleep(1000); // 模擬工作負載} catch (InterruptedException e) {e.printStackTrace();}}
}
2.
synchronized
修飾靜態方法
與同步實例方法類似,但同步靜態方法鎖定的是類的Class對象,而不是某個特定的對象實例。這意味著對于整個類的所有實例,同一時間只能有一個線程執行該靜態同步方法。
public class SynchronizedExample {public static synchronized void synchronizedStaticMethod() {// 同步代碼塊System.out.println(Thread.currentThread().getName() + " is executing synchronized static method.");try {Thread.sleep(1000); // 模擬工作負載} catch (InterruptedException e) {e.printStackTrace();}}
}
synchronizedStaticMethod
方法是靜態同步的,所以無論哪個實例調用了這個方法,都會鎖定該類的Class對象。
3.
synchronized
修飾代碼塊
有時你可能不想同步整個方法,而是只同步其中的一部分代碼。這時可以使用同步代碼塊,它可以指定要鎖定的對象。
public class SynchronizedExample {private final Object lock = new Object();public void someMethod() {synchronized(lock) {// 需要同步的代碼塊System.out.println(Thread.currentThread().getName() + " is executing synchronized block.");try {Thread.sleep(1000); // 模擬工作負載} catch (InterruptedException e) {e.printStackTrace();}}}
}
在這個例子中,我們定義了一個私有的lock對象,并在需要同步的代碼塊周圍使用了synchronized(lock)。這種方式允許更細粒度地控制哪些代碼需要同步,同時減少了不必要的同步開銷。
二、Java對象結構
學習
Java
對象結構時,需要有 JVM的相關知識作為背景。
Java對象(Object實例)結構包括三部分:對象頭
、對象體
和對齊字節
。
1. 對象頭
對象頭包括三個字段:
Mark Word(標記字)
:用于存儲自身運行時的數據例,如GC標志位、哈希碼、鎖狀態等信息。Class Pointer(類對象指針)
:用于存放此對象的元數據(InstanceKlass)的地址。虛擬機通過此指針可以確定這個對象是哪個類的實例。Array Length(數組長度)
:如果對象是一個Java數組,那么此字段必須有,用于記錄數組長度的數據;如果對象不是一個Java數組,那么此字段不存在,所以這是一個可選字段。
2. 對象體
對象體包含了對象的實例變量(成員變量),用于成員屬性值,包括父類的成員屬性值。這部分內存按4字節對齊。
3. 對齊字節
對齊字節也叫作填充對齊,其作用是用來保證Java對象在所占內存字節數為8的倍數(8N bytes)。
并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。當對象實例數據部分沒有對齊(8字節的整數倍)時,就需要通過對齊填充來補全。
HotSpot VM
的內存管理要求對象起始地址必須是8字節的整數倍。對象頭本身是8的倍數,當對象的實例變量數據不是8的倍數,需要填充數據來保證8字節的對齊。
4. 對象頭中的字段長度
Mark Word
、Class Pointer
、Array Length
字段的長度都為JVM
的一個Word
(字)大小。
在32位JVM
虛擬機中,字段長度都是是32位的;在64位JVM虛擬機中,字段長度都是64位的。
對于對象指針而言,如果JVM中對象數量過多,使用64位的指針將浪費大量內存。通過簡單統計,64位的JVM將會比32位的JVM多耗費50%的內存。
為了節約內存可以使用選項+UseCompressedOops
開啟指針壓縮。選項UseCompressedOops
中的Oop
部分為Ordinary object pointer
(普通對象指針)的縮寫。
如果開啟UseCompressedOops
選項,以下類型的指針將從64位壓縮至32位:
- Class對象的屬性指針(即靜態變量)。
- Object對象的屬性指針(即成員變量)。
- 普通對象數組的元素指針。
當然,也不是所有的指針都會壓縮,一些特殊類型的指針不會壓縮,比如指向PermGen
(永久代)的Class
對象指針(JDK 8中指向元空間的Class對象指針)、本地變量、堆棧元素、入參、返回值和NULL指針等。
Mark Word
的位長度不會受到OOP對象指針壓縮選項的影響。
在堆內存小于32GB的情況下,64位虛擬機的
UseCompressedOops
選項是默認開啟的,該選項表示開啟Oop對象的指針壓縮,會將原來64位的Oop對象指針壓縮為32位。
- 手動開啟Oop對象指針壓縮的Java指令為:
java -XX:+UseCompressedOops mainclass
- 手動關閉Oop對象指針壓縮的Java指令為:
java -XX:-UseCompressedOops mainclass
5. Mark Word 的結構信息
Java內置鎖的涉及很多重要信息,這些都存放在對象結構中,并且存放于對象頭的Mark Word字段中。Mark Word的位長度為JVM的一個Word大小,也就是說32位JVM的Mark Word為32位,4位JVM的Mark Word為64位。
Java內置鎖的狀態總共有4種,級別由低到高依次為:無鎖
、偏向鎖
、輕量級鎖
和重量級鎖
。
其實在JDK 1.6之前,Java內置鎖還是一個重量級鎖,是一個效率比較低下的鎖。
在JDK 1.6之后,JVM為了提高鎖的獲取與釋放效率,對synchronized的實現進行了優化,引入了偏向鎖、輕量級鎖的實現,從此以后Java內置鎖的狀態就有了4種(無鎖
、偏向鎖
、輕量級鎖
和重量級鎖
),并且這4種狀態會隨著競爭的情況逐漸升級,而且是不可逆的過程,即不可降級
,也就是說只能進行鎖升級(從低級別到高級別)。
1. 不同鎖狀態下的 Mark Word 字段結構
Mark Word字段的結構與Java內置鎖的狀態強相關。為了讓Mark Word字段存儲更多的信息,JVM將Mark Word的最低兩個位設置為Java內置鎖狀態位,不同鎖狀態下的32位Mark Word結構:
64位的Mark Word與32位的Mark Word結構相似:
2. 64 位 Mark Word 的構成
由于目前主流的JVM都是64位,使用64位的Mark Word,接下來對64位的Mark Word中各部分的內容做具體介紹。
lock
:鎖狀態標記位,占兩個二進制位,由于希望用盡可能少的二進制位表示盡可能多的信息,所以設置了lock標記。該標記的值不同,整個Mark Word表示的含義不同。biased_lock
:對象是否啟用偏向鎖標記,只占1個二進制位。為1時表示對象啟用偏向鎖,為0時表示對象沒有偏向鎖。
lock
和biased_lock
兩個標記位組合在一起,共同表示 Object實例處于什么樣的鎖狀態。二者組合的含義如下:
-
age
:4位的Java對象分代年齡。在GC中,如果對象在Survivor區復制一次,年齡增加1。當對象達到設定的閾值時,將會晉升到老年代。默認情況下,并行GC的年齡閾值為15,并發GC的年齡閾值為6。由于age只有4位,因此最大值為15,這就是-XX:MaxTenuringThreshold選項最大值為15的原因。 -
identity_hashcode
:31位的對象標識HashCode(哈希碼)采用延遲加載技術,當調用Object.hashCode()方法或者System.identityHashCode()方法計算對象的HashCode后,其結果將被寫到該對象頭中。當對象被鎖定時,該值會移動到Monitor(監視器)中。 -
thread
:54位的線程ID值為持有偏向鎖的線程ID。 -
epoch
:偏向時間戳。 -
ptr_to_lock_record
:占62位,在輕量級鎖的狀態下指向棧幀中鎖記錄的指針。 -
ptr_to_heavyweight_monitor
:占62位,在重量級鎖的狀態下,指向對象監視器的指針。
32位的Mark Word與64位Mark Word結構相似,這里不再贅述。
6. 使用 JOL 工具查看對象的布局
三、Java 內置鎖機制
3.1 內置鎖的演進過程
在JDK 1.6版本之前,所有的Java內置鎖都是重量級鎖。重量級鎖會造成CPU在用戶態和核心態之間頻繁切換,所以代價高、效率低。
JDK 1.6版本為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”實現
。所以,在JDK 1.6版本里內置鎖一共有4種狀態:無鎖狀態
、偏向鎖狀態
、輕量級鎖狀態
和重量級鎖狀態
,這些狀態隨著競爭情況逐漸升級。內置鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種能升級卻不能降級的策略,其目的是為了提高獲得鎖和釋放鎖的效率。
1. 無鎖狀態
當一個對象剛剛創建時,它處于無鎖狀態(也稱為自由狀態)。這意味著沒有任何線程試圖獲取該對象的鎖,因此可以認為它是自由訪問的。偏向鎖標識位是0、鎖狀態01。無鎖狀態下對象的Mark Word如下:
2. 偏向鎖狀態
偏向鎖是JVM的一種優化技術,主要針對鎖只會被單個線程持有的場景。它的核心思想是假設鎖總是由同一個線程獲得,從而避免每次進入同步塊時都進行昂貴的原子操作(如CAS操作)。
【重點】
- “偏向”:指的是內置鎖會偏向于當前已經占有過自己的線程。
- 無鎖轉變為偏向鎖的過程:
- 無鎖轉向偏向鎖 :當
線程A
第一次嘗試獲取內置鎖時,如果此時對象處于無鎖狀態,并且沒有其他線程競爭該鎖,則JVM會將該對象的鎖狀態從無鎖轉變為偏向鎖。在轉變為偏向鎖的過程中,JVM會在對象頭中標記該鎖為偏向鎖,并記錄下持有該鎖的線程ID。 - 嘗試獲取偏向鎖 :
線程A
再次嘗試獲取該內置鎖時,可以直接進入,無需執行任何同步操作,因為系統“偏向”了這個線程。 - 撤銷偏向鎖:在
線程 A
第二次獲取內置鎖之前,線程 B
嘗試獲取該內置鎖,那么當前的偏向鎖會被撤銷,轉而升級為輕量級鎖或者根據競爭情況進一步升級為重量級鎖。
- 無鎖轉向偏向鎖 :當
- 原理:如果不存在線程競爭的一個線程獲得了鎖,那么鎖就進入偏向狀態,此時Mark Word的結構變為偏向鎖結構,鎖對象的鎖標志位(lock)被改為01,偏向標志位(biased_lock)被改為1,然后線程的ID記錄在鎖對象的Mark Word中(使用CAS操作完成)。以后該線程獲取鎖的時候判斷一下線程ID和標志位,就可以直接進入同步塊,連CAS操作都不需要,這樣就省去了大量有關鎖申請的操作,從而也就提升了程序的性能。
偏向鎖狀態下對象的Mark Word具體如下:
3. 輕量級鎖狀態
- 當鎖處于偏向鎖狀態,但是鎖又被另一個線程所企圖搶占時,偏向鎖就會升級為輕量級鎖。企圖搶占的線程會通過
自旋
的形式嘗試獲取鎖,不會阻塞搶鎖線程,以便提高性能。 - 兩個線程公平競爭,哪個線程先占有鎖對象,鎖對象的Mark Word就指向哪個線程的棧幀中的鎖記錄。
輕量級鎖狀態下對象的Mark Word如圖所示:
自旋的基本思想是讓一個線程不斷地檢查某個條件是否滿足,而不是直接進入阻塞狀態。
如果持有鎖的線程能在很短時間內釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要不斷地檢查某個條件是否滿足,等持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內核切換的消耗。
但是,線程自旋是需要消耗 CPU的,如果一直獲取不到鎖,則線程也不能一直占用CPU自旋做無用功,所以需要設定一個自旋等待的最大時間。
輕量級鎖主要有兩種:普通自旋鎖
和自適應自旋鎖
。
普通自旋鎖
所謂普通自旋鎖,就是指當有線程來競爭鎖時,搶鎖線程會在原地循環等待,而不是被阻塞,直到那個占有鎖的線程釋放鎖之后,這個搶鎖線程才可以獲得鎖。
默認情況下,自旋的次數為10次,用戶可以通過-XX:PreBlockSpin選項來進行更改。
自適應自旋鎖
所謂自適應自旋鎖,就是等待線程空循環的自旋次數并非是固定的,而是會動態地根據實際情況來改變自旋等待的次數,自旋次數由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。
自適應自旋鎖的大概原理是:
-
如果搶鎖線程在同一個鎖對象上之前成功獲得過鎖,那么JVM就會認為這次自旋也很有可能再次成功,因此允許自旋等待持續相對更長的時間。
-
如果對于某個鎖,搶鎖線程在很少成功獲得過,那么JVM將可能減少自旋時間甚至省略自旋過程,以避免浪費處理器資源。
自適應自旋解決的是“鎖競爭時間不確定”的問題。自適應自旋假定不同線程持有同一個鎖對象的時間基本相當,競爭程度趨于穩定。總的思想是:根據上一次自旋的時間與結果調整下一次自旋的時間。
JDK 1.6的輕量級鎖使用的是普通自旋鎖,且需要使用-XX:+UseSpinning選項手工開啟。
JDK 1.7后,輕量級鎖使用自適應自旋鎖,JVM啟動時自動開啟,且自旋時間由JVM自動控制。
4. 重量級鎖狀態
重量級鎖會讓其他申請的線程之間進入阻塞,性能降低。重量級鎖也就叫同步鎖,這個鎖對象Mark Word再次發生變化,會指向一個監視器對象,該監視器對象用集合的形式來登記和管理排隊的線程。重量級鎖狀態下對象的Mark Word具體
在JVM中,每個對象都關聯一個監視器,這里的對象包含了Object實例和Class實例。監視器是一個同步工具,相當于一個許可證,拿到許可證的線程即可以進入臨界區進行操作,沒有拿到則需要阻塞等待。重量級鎖通過監視器的方式保障了任何時間只允許一個線程通過受到監視器保護的臨界區代碼。
核心原理
JVM中每個對象都會有一個監視器,監視器和對象一起創建、銷毀。監視器相當于一個用來監視這些線程進入的特殊房間,其義務是保證(同一時間)只有一個線程可以訪問被保護的臨界區代碼塊。
本質上,監視器是一種同步工具,也可以說是一種同步機制,主要特點是:
-
同步
。監視器所保護的臨界區代碼是互斥地執行的。一個監視器是一個運行許可,任一個線程進入臨界區代碼都需要獲得這個許可,離開時把許可歸還。 -
協作
。監視器提供Signal機制,允許正持有許可的線程暫時放棄許可進入阻塞等待狀態,等待其他線程發送Signal去喚醒;其他擁有許可的線程可以發送Signal,喚醒正在阻塞等待的線程,讓它可以重新獲得許可并啟動執行。