基本概念
- 線程安全問題三個要素:多線程、共享資源、非原子性操作;
- 產生的根本原因:多條線程同時對一個共享資源進行非原子性操作;
Synchronized
解決線程安全問題的方式:通過互斥鎖將多線程的并行執行變為單線程串行執行,同一時刻只讓一條線程執行,也就是當多條線程同時執行一段被互斥鎖保護的代碼(臨界資源)時,需要先獲取鎖,這時只會有一個線程獲取到鎖資源成功執行,其他線程將陷入等待的狀態,直到當前線程執行完畢釋放鎖資源之后,其他線程才能執行;Synchronized
可以保證可見性和有序性,但無法禁止指令重排序;
Synchronized鎖粒度及應用方式
Synchronized鎖的三種粒度
- 鎖粒度:
synchronized
本質上是通過對象來加鎖的,根據不同的對象類型可分為不同的鎖粒度;this
鎖:當前實例鎖;object
鎖:對象實例鎖;class
鎖:對象鎖;
應用方式
-
修飾實例成員方法:使用的是
this
鎖類型,這個this
代表的是當前new
出來的對象;synchronized void method() {//業務代碼 }
-
修飾靜態成員方法:使用的是
this
鎖類型,但由于靜態成員屬于類對象,所以這個this
代表的是class
對象;synchronized static void method() {//業務代碼 }
-
修飾代碼塊:修飾代碼塊時,可以指定鎖對象,可以將任意
class
類對象做為鎖資源;synchronized(SyncIncrDemo.class) {//業務代碼 }
-
使用
String
對象加鎖:因為修飾代碼塊時,可以將任意class
類對象做為鎖資源,而JAVA中字符串是以Sting
對象存儲使用的,并且在JVM
里有個字符串常量池,用于存儲字符串,那么相同值的字符串變量的引用地址可以是同一個,基于這個特性我們可以將synchronized
鎖粒度變細;如:將每一個訂單的ID轉化為字符串然后進行加鎖,這樣就能降低鎖粒度提高系統并發,但在加鎖時需要考慮加鎖的
String
對象是不是同一個,需要考慮String
對象使用的是堆中對象還是字符串常量池中的對象,若是堆中對象需要使用.intern()
將堆中對象刷入字符串常量池中;/*** @author xrl* @date 2024/6/25 22:47*/ public class Demo1 {public static void main(String[] args) {new Thread(() ->t1(111L), "AAA").start();new Thread(() ->t2(111L), "BBB").start();new Thread(() -> t2(111L), "CCC").start();}public static void t1(Long orderId){// 不能使用 String lock = orderId + "; 原因是orderId是入參,而方法可能在多個地方調用// 所以編譯器不會使用常量折疊技術對其進行優化,編譯后的代碼會被轉化為new StringBuilder().append(orderId).append("").toString()String lock = new String(orderId + "").intern();synchronized (lock){System.out.println(Thread.currentThread().getName() + "拿到鎖");try {System.out.println("t1業務執行");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "釋放鎖");}}public static void t2(Long orderId){String lock = new String(orderId + "").intern();synchronized (lock){System.out.println(Thread.currentThread().getName() + "拿到鎖");try {System.out.println("t2業務執行");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "釋放鎖");}} }
-
實現原理
Synchronized
是基于Monitor
(管程)對象實現的;- 獲取鎖:進入管程對象(顯式型:
monitorenter
指令); - 釋放鎖:退出管程對象(顯示型:
monitorexit
指令);
- 獲取鎖:進入管程對象(顯式型:
synchronized
修飾方法使用時是隱式同步的,是通過調用指令,讀取運行時常量池中的方法ACC_SYNCHRONIZED
標識實現的,也就無法通過javap
反編譯看到進入/退出管程對象的指令;
JAVA對象的內存布局
-
分為三個區域:對象頭、實例數據、對齊填充;
-
對象頭:存儲
MarkWord
和類型指針(ClassMetadataAddress/KlassWord
);如果為數組對象,還會存數組長度(ArrayLength
)-
主要包含:
unused
未使用的空間、HashCode
、age
分代年齡、biased_lock
是否偏向鎖、lock
鎖標記位、ThreadID
持有偏向鎖的線程ID、epoch
偏向鎖時間戳、ptr_to_lock_record
指向線程本地棧中lock_record
的指針、ptr_to_heavyweight_monitor
指向堆中monitor
對象的指針; -
64位系統中
MarkWord
結構
-
-
實例數據:存放當前對象的屬性成員信息,以及父類屬性成員信息;
-
對齊填充:虛擬機要求對象起始地址必須是
8byte
的整數倍,避免減少堆內存的碎片空間,并且方便操作系統讀取;
-
monitor
對象
-
概念:
monitor
本質是一個特殊的對象,存在于堆中,并且是線程私有的- 每個
java
對象都存在一個monitor
對象與之關聯,當一個monitor
被某個線程持有后,便會處于鎖定狀態;由ObjectMonitor
實現,具體代碼位于HotSpot
源碼的ObjectMonitor.hpp
文件中;
-
與線程之間的關聯:每個線程都有一個可用的
monitor record
列表,同時也存在一個全局的可用列表,每一個鎖住的對象,都會和monitor
關聯(對象頭的MarkWord
中的ptr_to_heavyweight_monitor
,指向monitor
的起始地址),同時monitor
中有一個Owner
字段,存放擁有該鎖的線程唯一標識,表示該鎖被這個線程占用; -
內部結構:
Contention List
:競爭隊列,**存放所有請求鎖的線程(**后續1.8版本中的_cxq
);Entry List
:一個雙向鏈表,存放Contention List
中有資格成為候選資源的線程;Wait Set
:哈希表,調用Object.wait()
方法后,被阻塞的線程被放置在這里;OnDeck
:任意時刻,最多只有一個線程正在競爭鎖資源,該線程被稱為OnDeck
;Owner
:擁有這個monitor record
線程的唯一標識,為NULL
說明沒有被占用;!Owner
:當前釋放鎖的線程;RcThis
:表示blocked
阻塞或waiting
等待在該monitor record上
的線程個數;Nest
:用來實現重入鎖的計數。Candidate
:用來避免不必要的阻塞或等待線程喚醒,因為每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程,喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒,然后因為競爭鎖失敗又被阻塞),從而導致性能嚴重下降。Candidate
只有兩種可能的值,0
表示沒有需要喚醒的線程;1
表示要喚醒一個繼任線程來競爭鎖;HashCode
:保存從對象頭拷貝過來的HashCode
值(可能還包含GC age
)。EntryQ
:關聯一個系統互斥鎖(semaphore
),阻塞所有試圖鎖住monitor record
失敗的線程;
-
使用流程:
- 每個獲取鎖或等待鎖的線程都會被封裝成
ObjectWaiter
對象; Monitor
有兩個隊列_WaitSet
和_EntryList
,用來保存ObjectWaiter
對象列表;- 當多個線程同時訪問一段同步代碼時,獲取到鎖的對象會進入到
_owner
區域,并將owner
變量設置為當前線程的唯一標識,同時nest
計數器加1
,沒有獲取到鎖的對象會加入_EntryList
隊列中等待; - 若線程調用
Object.wait()
方法,會釋放當前持有的monitor
,owenr
變量回復為null
,同時nest
計數器減1,并將線程放入到waitSet
集合中等待被喚醒; - 當調用
Monitor
對象的notify()
或notifyAll()
方法來喚醒WaitSet
中的等待線程時,會將等待線程移動到EntryList
隊列中等待獲取鎖的機會;
- 每個獲取鎖或等待鎖的線程都會被封裝成
修飾代碼塊的原理
-
反編譯代碼:
-
源代碼:
public class SyncDemo{int i;public void incr(){synchronized(this){i++;}} }
-
字節碼,
javac SyncDemo.java
javap -p -v -c SyncDemo.class
:Classfile /Users/xrl/nacos/SyncDemo.classLast modified 2024-5-16; size 388 bytesMD5 checksum 0c87d23a96c5f67c7d97908dbd338f95Compiled from "SyncDemo.java" public class SyncDemominor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER Constant pool:#1 = Methodref #4.#18 // java/lang/Object."<init>":()V#2 = Fieldref #3.#19 // SyncDemo.i:I#3 = Class #20 // SyncDemo#4 = Class #21 // java/lang/Object#5 = Utf8 i#6 = Utf8 I#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 incr#12 = Utf8 StackMapTable#13 = Class #20 // SyncDemo#14 = Class #21 // java/lang/Object#15 = Class #22 // java/lang/Throwable#16 = Utf8 SourceFile#17 = Utf8 SyncDemo.java#18 = NameAndType #7:#8 // "<init>":()V#19 = NameAndType #5:#6 // i:I#20 = Utf8 SyncDemo#21 = Utf8 java/lang/Object#22 = Utf8 java/lang/Throwable {int i;descriptor: Iflags:public SyncDemo();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0/*-------synchronized修飾incr()中代碼塊,反匯編之后得到的字節碼文件--------*/public void incr();descriptor: ()Vflags: ACC_PUBLICCode:stack=3, locals=3, args_size=10: aload_01: dup2: astore_13: monitorenter // monitorenter進入同步4: aload_05: dup6: getfield #2 // Field i:I9: iconst_110: iadd11: putfield #2 // Field i:I14: aload_115: monitorexit // monitorexit退出同步16: goto 2419: astore_220: aload_121: monitorexit // monitorexit退出同步22: aload_223: athrow24: returnException table:from to target type4 16 19 any19 22 19 anyLineNumberTable:line 5: 0line 6: 4line 7: 14line 8: 24StackMapTable: number_of_entries = 2frame_type = 255 /* full_frame */offset_delta = 19locals = [ class SyncDemo, class java/lang/Object ]stack = [ class java/lang/Throwable ]frame_type = 250 /* chop */offset_delta = 4 }
-
-
從字節碼中可知,
synchronized
修飾代碼塊,是基于進入管程monitorenter
和退出管程monitorexit
指令實現的,其中monitorenter
指令指向同步代碼塊的開始位置,monitorexit
指令則指明同步代碼塊的結束位置;
有兩條monitorexit
指令的原因是解決方法異常結束時,鎖釋放問題;
修飾方法的原理
-
反編譯代碼:
-
源代碼:
public class SyncDemo {int i;public synchronized void incr() {i++;} }
-
字節碼:
Classfile /Users/xrl/nacos/SyncDemo.classLast modified 2024-5-16; size 388 bytesMD5 checksum 0c87d23a96c5f67c7d97908dbd338f95Compiled from "SyncDemo.java" public class SyncDemominor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER Constant pool:#1 = Methodref #4.#18 // java/lang/Object."<init>":()V#2 = Fieldref #3.#19 // SyncDemo.i:I#3 = Class #20 // SyncDemo#4 = Class #21 // java/lang/Object#5 = Utf8 i#6 = Utf8 I#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 incr#12 = Utf8 StackMapTable#13 = Class #20 // SyncDemo#14 = Class #21 // java/lang/Object#15 = Class #22 // java/lang/Throwable#16 = Utf8 SourceFile#17 = Utf8 SyncDemo.java#18 = NameAndType #7:#8 // "<init>":()V#19 = NameAndType #5:#6 // i:I#20 = Utf8 SyncDemo#21 = Utf8 java/lang/Object#22 = Utf8 java/lang/Throwable {int i;descriptor: Iflags:public SyncDemo();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0public void incr();descriptor: ()Vflags: ACC_PUBLICCode:stack=3, locals=3, args_size=10: aload_01: dup2: astore_13: monitorenter4: aload_05: dup6: getfield #2 // Field i:I9: iconst_110: iadd11: putfield #2 // Field i:I14: aload_115: monitorexit16: goto 2419: astore_220: aload_121: monitorexit22: aload_223: athrow24: returnException table:from to target type4 16 19 any19 22 19 anyLineNumberTable:line 5: 0line 6: 4line 7: 14line 8: 24StackMapTable: number_of_entries = 2frame_type = 255 /* full_frame */offset_delta = 19locals = [ class SyncDemo, class java/lang/Object ]stack = [ class java/lang/Throwable ]frame_type = 250 /* chop */offset_delta = 4 } SourceFile: "SyncDemo.java" xrl@xrldeMacBook-Air nacos % open 。/ The file /Users/xrl/nacos/。 does not exist. xrl@xrldeMacBook-Air nacos % open ./ xrl@xrldeMacBook-Air nacos % javac SyncDemo.java xrl@xrldeMacBook-Air nacos % javap -p -v -c SyncDemo.class Classfile /Users/xrl/nacos/SyncDemo.classLast modified 2024-5-16; size 276 bytesMD5 checksum ef45d44f86eef93281fefeda549e1283Compiled from "SyncDemo.java" public class SyncDemominor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER Constant pool:#1 = Methodref #4.#14 // java/lang/Object."<init>":()V#2 = Fieldref #3.#15 // SyncDemo.i:I#3 = Class #16 // SyncDemo#4 = Class #17 // java/lang/Object#5 = Utf8 i#6 = Utf8 I#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 incr#12 = Utf8 SourceFile#13 = Utf8 SyncDemo.java#14 = NameAndType #7:#8 // "<init>":()V#15 = NameAndType #5:#6 // i:I#16 = Utf8 SyncDemo#17 = Utf8 java/lang/Object {int i;descriptor: Iflags:public SyncDemo();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0public synchronized void incr();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=3, locals=1, args_size=10: aload_01: dup2: getfield #2 // Field i:I5: iconst_16: iadd7: putfield #2 // Field i:I10: returnLineNumberTable:line 5: 0line 6: 10 }
-
-
由字節碼可知
synchronized
修飾的方法,并沒有出現monitorenter
指令和monitorexit
指令,取得代之的是:在flags: ACC_PUBLIC
之后增加了一個ACC_SYNCHRONIZED
標識。這個標識指明了當前方法是一個同步方法,JVM
通過這個ACC_SYNCHRONIZED
訪問標志,來辨別一個方法是否為同步方法,從而執行相應的同步調用;
JVM對synchronized的優化
鎖狀態
JDK1.6
之后,synchronized
鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖;隨著線程的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級一般是單向的,也就是說只能從低到高升級,通常不會出現鎖的降級(只針對用戶線程,在STW
可能會發生鎖降級)。
無鎖狀
new
一個對象時,會默認啟動匿名偏向鎖,但為了避免JVM
在啟動階段大量創建對象,從而導致偏向鎖競爭過多影響性能,則在JVM
啟動階段會次用延遲偏向鎖策略,也就是等待一定時間后再開啟偏向鎖,默認為4
秒,可通過-XX:BiasedLockingStartupDelay = xx
設置;- 對于一個新創建的對象,由于在沒有成為真正偏向鎖之前,對象頭
markword
中的線程ID會一直為空,這種被稱為概念上的無鎖對象,但markword
的鎖標識為101
;
偏向鎖
- 為了減少同一線程獲取鎖的代價,如
CAS
操作帶來的耗時等; - 核心思想:如果一個線程獲得了鎖,那么鎖就進入偏向模式,此時
Mark Word
的結構也變為偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作; - 操作流程:
Load-and-test
,就是簡單判斷一下當前線程id
是否與Markword
中的線程id
是否一致;- 如果一致,則說明此線程持有的偏向鎖,沒有被其他線程覆蓋,直接執行后續代碼;
- 如果不一致,則要檢查一下對象是否還屬于可偏向狀態,即檢查“是否偏向鎖”標志位;
- 如果還未偏向,則利用
CAS
操作來競爭鎖,再次將ID
放進去,即重復第一次獲取鎖的動作;
- 偏向鎖主要是在鎖競爭不激烈的場合對性能的提升,但是對于鎖競爭比較激烈的場合,偏向鎖作用就很小了,甚至成為累贅,可以通過
XX:-UseBiasedLocking
命令關閉;
撤銷過程
- 在一個安全點停止擁有鎖的線程;
- 遍歷線程棧,如果存在鎖記錄的話,需要修復鎖記錄和
Markword
,使其變成無鎖狀; - 喚醒當前線程,將當前鎖升級成輕量級鎖;
膨脹過程
- 當首個線程進程嘗試獲取鎖時,會通過
CAS
操作,將自己的threadID
設置到MarkWord
中,如果設置成功,則證明拿到偏向鎖; - 當線程再次嘗試獲取鎖時,發現自己的線程ID和對象頭中的偏向線程ID一致,則在當前線程棧的
lock record
鎖記錄中添加一個空的Displaced Mark Word
(表示的是原先持有偏向鎖的線程ID和相關信息被移動到哪里的標記),并不需要CAS
操作; - 重新偏向,當其他線程進入同步塊時,發現偏向線程不是自己,則進入偏向鎖撤銷的邏輯;當達到全局安全點時,如果發現偏向線程掛了,那就把偏向鎖撤銷,并將對象頭內的
MarkWord
修復為無鎖狀態,自己嘗試獲取偏向鎖; - 可如果原本的偏向線程還存活,重新偏向失敗后,鎖開始膨脹為輕量級鎖,原來的線程仍然持有鎖;
輕量級鎖
鎖膨脹過程
- 根據
markWork
判斷是否有線程持有鎖,如果有則在當前線程棧中創建一個lock record
復制markWord
,并通過CAS
將當前線程棧的lock record
地址放入對象頭中,如果成功則說明獲取到輕量級鎖; - 如果失敗則說明鎖已經被其他線程持有了,此時記錄線程的重入次數(把
ock record
的markword
設置為null
),并進入自適應自旋; - 如果自旋到一定的次數后還未獲取到鎖,則說明目前競爭較重,則膨脹為重量級鎖;
自旋
- 自旋鎖:是指當一個線程在獲取鎖的時候,如果鎖已經被其它線程獲取,那么該線程將循環等待,然后不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出循環;
- 存在的問題:
- 若加鎖代碼執行慢或鎖競爭激烈,則需要等待較長時間,會導致CPU空轉,消耗大量CPU資源;
- 非公平鎖,若存在多個線程自旋等待同一把鎖,可能對導致有些線程一致獲取不到鎖;
- 解決辦法:通過
-XX:PreBliockSpin
給線程空循環設置一個次數,默認是10
次,或者自旋線程數超過CPU
核數的一半時,鎖會再次膨脹升級為重量級鎖;
- 存在的問題:
- 自適應自旋鎖:線程空循環次數不固定,而是會根據實際情況動態調整;
- 在大部分情況下,已經獲取到鎖的線程再次嘗試獲取鎖,大概率能成功拿到該鎖,所以虛擬機會延長這個線程的自旋次數;
重量級鎖
- 當出現較大競爭鎖膨脹為重量級鎖時,對象頭的
markword
指向堆中的monitor
,此時線程會封裝為一個ObjectWaiter
對象,并插入到monitor
的_cxq
競爭隊列中然后掛起線程,等待鎖釋放; - 當持有鎖的線程釋放后,會將
_cxq
競爭隊列中的ObjectWaiter
對象,移到EntryList
中,并隨機挑選一個對象也就是一個線程喚醒,被選中的線程稱之為Heir Presumptive
假定繼承人,之后Heir Presumptive
會去嘗試獲取鎖,但在這段期間其他線程頁可能嘗試獲取鎖,所以Heir Presumptive
不一定可以獲取到鎖,當沒有獲取到鎖后那么它會退回到等待隊列中,成為EntryList
中的最后一個對象; - 當線程獲取到鎖后,調用
Object.wait()
方法后,會將線程加入到WaitSet
中,當被Object.notify()
喚醒后,會將線程從WaitSet
移動到_cxq
或EntryList
中去,并且由于Object.wait()、Object.notify()
方法主要依賴于Monitor
對象實現的,所以鎖對象調用這兩個方法時的鎖狀態如果為偏向鎖或輕量級鎖,則會先膨脹成重量級鎖;
鎖膨脹過程
- 無鎖態:
JVM
啟動后-XX:BiasedLockingStartupDelay
(默認四秒)內的普通對象和四秒后的匿名偏向鎖對象; - 偏向鎖:只有一個線程進入臨界區;
- 偏向鎖未開啟:直接膨脹為輕量級鎖;
- 將
MarkWord
中鎖標識位信息除外的其他所有信息copy
到自己的棧內存的Lock Record
中,再嘗試通過CAS
將MarkWord
中的ptr_to_lock_record
指向自己的棧內Lock Record
,替換成功則獲取到鎖,沒有則繼續自旋;
- 將
- 匿名偏向鎖:
- 將
MarkWord
中鎖標識位信息除外的其他所有信息copy
到自己的棧內存的Lock Record
中,并嘗試通過CAS
將自己的線程ID設置到MarkWord
中;這個線程,后續再次使用這個鎖時,無需進行加鎖和鎖釋放操作,只需要在Lock Record
添加一個空的MarkWord
; - 重新偏向:當嘗試獲取鎖時,發現線程ID與
MarkWord
中記錄的線程ID不相同,則進入偏向鎖撤銷的邏輯;當達到全局安全點時,發現之前持有鎖的線程執行完畢,則會發生偏向鎖撤銷(清除鎖記錄回歸無鎖態),然后線程可以通過CAS
將自己重新設置到MarkWord
中 - 重新偏向失敗:鎖膨脹為輕量級鎖,嘗試通過
CAS
將MarkWord
中的ptr_to_lock_record
指向自己的棧內Lock Record
,替換成功則獲取到鎖,沒有則繼續自旋;
- 將
- 調用
Object.wait()
方法,直接膨脹為重量級鎖;
- 偏向鎖未開啟:直接膨脹為輕量級鎖;
- 輕量級鎖:多個線程交替進入臨界區:
- 當出現重度競爭、耗時過長、自旋過多等情況時會膨脹為重量級鎖;
- 自旋線程數超過
CPU
核數的一半; - 自旋超過
-XX:PreBliockSpin
默認10
次;
- 自旋線程數超過
- 調用
Object.wait()
方法,直接膨脹為重量級鎖;
- 當出現重度競爭、耗時過長、自旋過多等情況時會膨脹為重量級鎖;
- 重量級鎖:多個線程同時進入臨界區;
鎖狀態的內存布局分析
-
引入架包:
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.16</version> </dependency>
-
MarkWord
結構: -
運行代碼:
package com.xrl.test;import org.openjdk.jol.info.ClassLayout;/*** @version [v1.0]* @author: [xrl]* @create: [2024/05/21 16:14]**/ public class ObjectHead {public String str;public static void main(String[] args) throws InterruptedException {/**無鎖態:虛擬機剛啟動時 new 出來的對象處于無鎖狀態**/ObjectHead obj = new ObjectHead();System.out.println(ClassLayout.parseInstance(obj).toPrintable());/**com.xrl.test.ObjectHead object internals:OFF SZ TYPE DESCRIPTION VALUE0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) // 對象頭 non-biasable (轉化為二進制最后三位表示,鎖標志位狀態:001)8 4 (object header: class) 0xf800c105 // ClassPointer指針12 4 java.lang.String ObjectHead.str null // 成員變量Instance size: 16 bytes // 共占用16個字節Space losses: 0 bytes internal + 0 bytes external = 0 bytes total*//**輕量級鎖:對于真正的無鎖態對象obj加鎖之后的對象處于輕量級鎖狀態**/synchronized (obj) {// 查看對象內部信息System.out.println(ClassLayout.parseInstance(obj).toPrintable());/**com.xrl.test.ObjectHead object internals:OFF SZ TYPE DESCRIPTION VALUE0 8 (object header: mark) 0x0000000309dcd9d8 (thin lock: 0x0000000309dcd9d8) // (轉化為二進制最后三位表示,鎖標志位狀態:000)8 4 (object header: class) 0xf800c10512 4 java.lang.String ObjectHead.str nullInstance size: 16 bytesSpace losses: 0 bytes internal + 0 bytes external = 0 bytes total*/}/**匿名偏向鎖:休眠4S后再創建出來的對象處于匿名偏向鎖狀態**/Thread.sleep(4000);ObjectHead obj1 = new ObjectHead();System.out.println(ClassLayout.parseInstance(obj1).toPrintable());/**com.xrl.test.ObjectHead object internals:OFF SZ TYPE DESCRIPTION VALUE0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0) //(轉化為二進制最后三位表示,鎖標志位狀態:101)8 4 (object header: class) 0xf800c10512 4 java.lang.String ObjectHead.str nullInstance size: 16 bytesSpace losses: 0 bytes internal + 0 bytes external = 0 bytes total*//**重量級鎖:調用wait方法之后鎖對象直接膨脹為重量級鎖狀態**/new Thread(() -> {try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}).start();Thread.sleep(1);synchronized (obj) {// 查看對象內部信息System.out.println(ClassLayout.parseInstance(obj).toPrintable());/*** com.xrl.test.ObjectHead object internals:* OFF SZ TYPE DESCRIPTION VALUE* 0 8 (object header: mark) 0x00007fe748016c0a (fat lock: 0x00007fe748016c0a) (轉化為二進制最后三位表示,鎖標志位狀態:010)* 8 4 (object header: class) 0xf800c105* 12 4 java.lang.String ObjectHead.str null* Instance size: 16 bytes* Space losses: 0 bytes internal + 0 bytes external = 0 bytes total*/}} } /*** 拋出異常原因:違法的監控狀態異常。當某個線程試圖等待一個自己并不擁有的對象(Obj)的監控器或者通知其他線程等待該對象(Obj)的監控器時,拋出該異常* Exception in thread "Thread-1" java.lang.IllegalMonitorStateException* at java.lang.Object.wait(Native Method)* at java.lang.Object.wait(Object.java:502)* at com.xrl.test.ObjectHead.lambda$main$0(ObjectHead.java:53)* at java.lang.Thread.run(Thread.java:750)*/
同步消除
- Java虛擬機在編譯代碼時,通過會對運行上下文進行掃描,從而去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的獲取鎖開銷;
鎖的可重入
-
一個線程獲得一個對象鎖后,運行他再次請求該對象鎖;
-
synchronized
是基于monitor
實現的,每次重入monitor
中的計數器會加一; -
當子類繼承父類時,子類可以通過可重入鎖,調用父類的同步方法;
class Parent {public synchronized void parentMethod() {System.out.println("Parent method");} }class Child extends Parent {public synchronized void childMethod() {System.out.println("Child method");parentMethod(); // 在子類中調用父類的同步方法} }public class Main {public static void main(String[] args) {Child child = new Child();child.childMethod();} }
其他機制
等待/喚醒機制
wait()、notify()、notifyAll()
這三個方法在使用時,必須處在synchronized
代碼塊或方法中,否則會拋出IllegalMonitorStateException
異常,這是由于這三個方法都依賴于monitor
對象實現這也是三個方法處在Object
對象中的原因,而synchronized
關鍵字決定著一個JAVA對象會不會生成monitor
對象;wait()、sleep()
方法的區別:wait()
方法會釋放當前持有的鎖,并將線程移入waitSet
中;sleep()
方法只會讓線程休眠并不會釋放鎖(類似于執行for(;;){}
死循環);
線程中斷機制
-
JDK1.2
遺棄Thread.stop()
后,JAVA就沒有提供強制性停止執行中線程的方法,而是提供了協調式的方式;//中斷線程(實例方法) public void Thread.interrupt(); //判斷線程是否被中斷(實例方法) public boolean Thread.isInterrupted(); //判斷是否被中斷并清除當前中斷狀態(靜態方法) public static boolean Thread.interrupted();
-
使用:我們可以通過調用
Thread.interrupt()
來進行線程中斷,但由于線程中斷是協調式的,所以他并不會去停止線程,而是需要我們手動進行中斷檢測并結束線程;并且當線程處于阻塞狀態或者嘗試執行一個阻塞操作時,我們調用線程中斷方法,執行中斷操作后會拋出InterruptedException
異常;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(){@Overridepublic void run(){try {// 判斷當前線程是否已中斷,執行后會對中斷狀態進行復位while (!Thread.interrupted()) {System.out.println("1111");// 線程阻塞,拋出異常// TimeUnit.SECONDS.sleep(1);}System.out.println("線程中斷");// 輸出false 說明中斷狀態復位System.out.println(this.isInterrupted());} catch (Exception e) {System.out.println(e);}}};t1.start();TimeUnit.SECONDS.sleep(2);// 中斷線程t1.interrupt();}
-
synchronized
與線程中斷:對于synchronized
而言,一個線程的執行只有兩種狀態,一種是獲取了鎖正在執行,一種是沒獲取到鎖在阻塞額等待;那么他們即使調用中斷線程的方法,也不會生效;
synchronized為什么不禁止指令重排序
synchronized
是通過互拆鎖的方式來保證線程安全的,它的本質是將多線程并行執行變成單線程的串行執行,而指令重排序可能導致的問題是多線程環境程序亂序執行的問題,所以指令重排序對synchronized
而言并不會存在亂序問題,反而可以提升串行執行時的性能;
synchronized性能不佳的原因
synchronized
是基于進入和退出Monitor
管程實現的,而Monitor
底層時依賴于操作系統的Mutex Lock
,所以在其獲取鎖或者釋放鎖的時候都需要經過操作系統的調用,會涉及到頻繁的用戶態與內核態之間的切換,從而導致性能不佳;- 但在并發競爭不高的情況下,由于
synchronized
幾種鎖狀態、鎖消除等技術的優化,synchronized
性能并不差;