【并發(fā)】并發(fā)鎖機制-深入理解synchronized(二)
成都創(chuàng)新互聯(lián)是一家網(wǎng)站設(shè)計公司,集創(chuàng)意、互聯(lián)網(wǎng)應(yīng)用、軟件技術(shù)為一體的創(chuàng)意網(wǎng)站建設(shè)服務(wù)商,主營產(chǎn)品:響應(yīng)式網(wǎng)站設(shè)計、品牌網(wǎng)站建設(shè)、網(wǎng)絡(luò)營銷推廣。我們專注企業(yè)品牌在網(wǎng)站中的整體樹立,網(wǎng)絡(luò)互動的體驗,以及在手機等移動端的優(yōu)質(zhì)呈現(xiàn)。做網(wǎng)站、網(wǎng)站制作、移動互聯(lián)產(chǎn)品、網(wǎng)絡(luò)運營、VI設(shè)計、云產(chǎn)品.運維為核心業(yè)務(wù)。為用戶提供一站式解決方案,我們深知市場的競爭激烈,認(rèn)真對待每位客戶,為客戶提供賞析悅目的作品,網(wǎng)站的價值服務(wù)。synchronized 高級篇(底層原理)
一、查看synchronized的字節(jié)碼指令序列
同步方法
同步代碼塊?
二、Monitor(管程/監(jiān)視器)
MESA模型
wait()的正確使用姿勢
notify() 和 notifyAll() 分別何時使用
關(guān)于 wait、notify、notifyAll的問題詳解
Java語言的內(nèi)置管程synchronized
Monitor機制在Java中的實現(xiàn)
圖解Java中的Monitor機制
【思考】synchronized加鎖加在對象上,鎖對象是如何記錄鎖狀態(tài)的??
三、對象的內(nèi)存布局
【了解】什么是對象頭?
【問】new Object() 在對象中占用幾個字節(jié)???
四、使用JOL工具查看內(nèi)存布局
導(dǎo)pom依賴
示例代碼?
運行結(jié)果?
下一節(jié)——synchronized底層鎖的優(yōu)化解析
synchronized是JVM內(nèi)置鎖,基于Monitor機制實現(xiàn)。
這個Monitor就是管程的意思,它可以控制線程,讓其陷入等待,或者將其喚醒!
synchronized 依賴底層操作系統(tǒng)的互斥原語Mutex(互斥量),它是一個重量級鎖,性能較低。
因為,有使用到操作系統(tǒng)底層的原語Mutex,我們只能通過系統(tǒng)調(diào)用來使用它!所以,CPU要從用戶態(tài)到內(nèi)核態(tài),它是一個很重的操作!
不過,在JVM內(nèi)置鎖在1.5之后版本做了重大的優(yōu)化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、自適應(yīng)自旋(Adaptive Spinning)等技術(shù)來減少鎖操作的開銷,內(nèi)置鎖的并發(fā)性能已經(jīng)基本與Lock持平。?
一、查看synchronized的字節(jié)碼指令序列根據(jù)一些測試報告,在數(shù)據(jù)量不是很大的情況下,synchronized的性能大約只比ReentrantLock 差10%-20%!?
Java虛擬機通過一個同步結(jié)構(gòu)支持方法和方法中的指令序列的同步:monitor
同步方法是通過方法中的access_flags(訪問標(biāo)志位)中設(shè)置ACC_SYNCHRONIZED標(biāo)志來實現(xiàn)。
同步代碼塊是通過?monitorenter?和?monitorexit?來實現(xiàn)。兩個指令的執(zhí)行是JVM通過調(diào)用操作系統(tǒng)的互斥原語mutex來實現(xiàn),被阻塞的線程會被掛起、等待重新調(diào)度,會導(dǎo)致“用戶態(tài)和內(nèi)核態(tài)”兩個態(tài)之間來回切換,對性能有較大影響。
同步方法private static int counter = 0;
public synchronized static void increment() {
counter++;
}
public synchronized static void decrement() {
counter--;
}
這里的synchronized加在方法上面,所以方法內(nèi)部的指令沒有發(fā)生變化!僅僅是加了一個標(biāo)志位!
這邊顯示的是0x0029,其實是0x0001 + 0x0008+ 0x0020
同步代碼塊?private static String lock = "";
public static void increment() {
synchronized (lock) {
counter++;
}
}
public static void decrement() {
synchronized (lock) {
counter--;
}
}
這里方法內(nèi)部的指令發(fā)生的改變!
【問】為什么monitorexit指令有2次??
第一個monitorexit指令是同步代碼塊正常釋放鎖的一個標(biāo)志
如果同步代碼塊中出現(xiàn)Exception或者Error,則會調(diào)用第二個monitorexit指令來保證釋放鎖
二、Monitor(管程/監(jiān)視器)Monitor在操作系統(tǒng)中就是管程,而在Java中,我們通常稱它為監(jiān)視器!
管程是指管理共享變量以及對共享變量操作的過程,讓它們支持并發(fā)。
在Java 1.5之前,Java語言提供的唯一并發(fā)語言就是管程,Java 1.5之后提供的SDK并發(fā)包也是以管程為基礎(chǔ)的!例如:JUC
synchronized關(guān)鍵字和wait()、notify()、notifyAll()這三個方法是Java中實現(xiàn)管程技術(shù)的組成部分。
MESA模型在管程的發(fā)展史上,先后出現(xiàn)過三種不同的管程模型——Hasen模型、Hoare模型和MESA模型。
現(xiàn)在正在廣泛使用的是MESA模型,介紹如下:
入口只允許一個線程通過,其余的現(xiàn)在入口等待隊列中等待!這樣子設(shè)計可以解決互斥的問題!?
進去之后,里面還提供了條件變量,每個條件變量都對應(yīng)有一個等待隊列!
條件變量和其等待隊列的作用是解決線程之間的同步問題!條件隊列里面存的東西,可以理解為“被wait()” 的線程。
wait()的正確使用姿勢對于MESA管程來說,有一個編程范式:
while(條件不滿足) {
??wait();
}
喚醒的時間和獲取到鎖繼續(xù)執(zhí)行的時間是不一致的,被喚醒的線程再次執(zhí)行時可能條件又不滿足了,所以循環(huán)檢驗條件。MESA模型的wait()方法還有一個超時參數(shù),為了避免線程進入等待隊列永久阻塞。?
我們可以看看Object類里面的對于?wait() 方法的注解描述:
確實需要將其放在循環(huán)里面。?
notify() 和 notifyAll() 分別何時使用滿足以下三個條件時,可以使用notify(),其余情況盡量使用notifyAll():
【面試題】notify() 和 notifyAll()方法的使用和區(qū)別_面向架構(gòu)編程的博客-博客https://blog.csdn.net/weixin_43715214/article/details/128665586
Java語言的內(nèi)置管程synchronizedJava 參考了 MESA 模型,語言內(nèi)置的管程(synchronized)對 MESA 模型進行了精簡。
MESA 模型中,條件變量可以有多個,Java 語言內(nèi)置的管程里只有一個條件變量。
模型如下圖所示:
Monitor機制在Java中的實現(xiàn)java.lang.Object 類定義了 wait(),notify(),notifyAll() 方法,這些方法的具體實現(xiàn),依賴于 ObjectMonitor 實現(xiàn),這是 JVM 內(nèi)部基于 C++ 實現(xiàn)的一套機制。
ObjectMonitor?其主要數(shù)據(jù)結(jié)構(gòu)如下(hotspot源碼ObjectMonitor.hpp):
ObjectMonitor() {
_header = NULL; //對象頭 markOop
_count = 0;
_waiters = 0,
_recursions = 0; // 鎖的重入次數(shù)
_object = NULL; //存儲鎖對象
_owner = NULL; // 標(biāo)識擁有該monitor的線程(當(dāng)前獲取鎖的線程)
_WaitSet = NULL; // 等待線程(調(diào)用wait)組成的雙向循環(huán)鏈表,_WaitSet是第一個節(jié)點
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多線程競爭鎖會先存到這個單向鏈表中 (FILO棧結(jié)構(gòu))
FreeNext = NULL ;
_EntryList = NULL ; //存放在進入或重新進入時被阻塞(blocked)的線程 (也是存競爭鎖失敗的線程)
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
其中涉及到3種鏈表——cxq、WaitSet、EntryList
但是從隊列中挑選一個線程進行喚醒,如何挑選?有沒有什么原則?
這一方面比較復(fù)雜!具體看下文?。?!
圖解Java中的Monitor機制首先,所有線程去競爭鎖,競爭失敗的線程會進入cxq(FILO)里面;
然后,持有鎖的線程,執(zhí)行后續(xù)邏輯,如果有?wait?方法,進入等待隊列waitSet;
接下來,可能繼續(xù)被喚醒,可能會進入cxq隊列(棧),也可能進入EntryList(這要看具體的策略?。?/p>
最后,再次競爭鎖的時候,可能從cxq中獲取,也可能從EntryList中獲?。?
默認(rèn)策略
【思考】synchronized加鎖加在對象上,鎖對象是如何記錄鎖狀態(tài)的??如果?EntryList為空,則將?cxq中的元素按原有順序插入到EntryList,并喚醒第一個線程,也就是當(dāng)EntryList為空時,是后來的線程先獲取鎖。(非公平?。?/p>
如果 EntryList不為空,直接從 EntryList 中喚醒線程。
鎖狀態(tài)是被記錄在每個對象的對象頭(Mark Word)中!具體看下文!
三、對象的內(nèi)存布局Hotspot虛擬機中,對象在內(nèi)存中存儲的布局可以分為三塊區(qū)域:對象頭(Header)、實例數(shù)據(jù)(Instance Data)和?對齊填充(Padding)
【了解】什么是對象頭?
- 對象頭:比如 hash碼,對象所屬的年代,對象鎖,鎖狀態(tài)標(biāo)志,偏向鎖(線程)ID,偏向時間,數(shù)組長度(數(shù)組對象才有)等。
- 實例數(shù)據(jù):存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息;
- 對齊填充:由于虛擬機要求?對象起始地址 必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對齊。
對象頭是對象中最復(fù)雜的部分!HotSpot虛擬機的對象頭包括:3個部分
(1)Mark Word?
用于存儲對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分?jǐn)?shù)據(jù)的長度在32位和64位的虛擬機中分別為32bit和64bit,官方稱它為“Mark Word”。
圖解32位的JVM的存儲情況
圖解64位的JVM的存儲情況
雖然它們在不同位數(shù)的JVM中長度不一樣,但是基本組成內(nèi)容是一致的。
- 鎖標(biāo)志位(lock):區(qū)分鎖狀態(tài),11時表示對象待GC回收狀態(tài), 只有最后2位鎖標(biāo)識(11)有效。
- biased_lock:是否偏向鎖,由于無鎖和偏向鎖的鎖標(biāo)識都是 01,沒辦法區(qū)分,這里引入一位的偏向鎖標(biāo)識位。
- 分代年齡(age):表示對象被GC的次數(shù),當(dāng)該次數(shù)到達閾值的時候,對象就會轉(zhuǎn)移到老年代。大是15,所以是四位!
- 對象的hashcode(hash):運行期間調(diào)用System.identityHashCode()來計算,延遲計算,并把結(jié)果賦值到這里。當(dāng)對象加鎖后,計算的結(jié)果31位不夠表示,在偏向鎖,輕量鎖,重量鎖,hashcode會被轉(zhuǎn)移到Monitor中。
- 偏向鎖的線程ID(JavaThread):偏向模式的時候,當(dāng)某個線程持有對象的時候,對象這里就會被置為該線程的ID。 在后面的操作中,就無需再進行嘗試獲取鎖的動作。
- epoch:偏向鎖在CAS鎖操作過程中,偏向性標(biāo)識,表示對象更偏向哪個鎖
- ptr_to_lock_record:輕量級鎖狀態(tài)下,指向棧中鎖記錄的指針。當(dāng)鎖獲取是無競爭的時,JVM使用原子操作而不是OS互斥。這種技術(shù)稱為輕量級鎖定。在輕量級鎖定的情況下,JVM通過CAS操作在對象的標(biāo)題字中設(shè)置指向鎖記錄的指針。
- ptr_to_heavyweight_monitor:重量級鎖狀態(tài)下,指向?qū)ο蟊O(jiān)視器Monitor的指針。如果兩個不同的線程同時在同一個對象上競爭,則必須將輕量級鎖定升級到Monitor以管理等待的線程。在重量級鎖定的情況下,JVM在對象的ptr_to_heavyweight_monitor設(shè)置指向Monitor的指針。
簡單的來說就是:
enum {
locked_value = 0, //00 輕量級鎖
unlocked_value = 1, //001 無鎖
monitor_value = 2, //10 監(jiān)視器鎖,也叫膨脹鎖,也叫重量級鎖
marked_value = 3, //11 GC標(biāo)記
biased_lock_pattern = 5 //101 偏向鎖
}
圖示如下:
(2)Klass Pointer
對象頭的另外一部分是klass類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。默認(rèn)開啟壓縮指針,4個字節(jié)(未開啟是8個)
(3)數(shù)組長度(只有數(shù)組對象有)
如果對象是一個數(shù)組, 那在對象頭中還必須有一塊數(shù)據(jù)用于記錄數(shù)組長度。?4字節(jié)
【問】new Object() 在對象中占用幾個字節(jié)???JDK8是默認(rèn)開啟壓縮指針,如果該對象是數(shù)組,則對象頭是16個字節(jié);反之,只是一個普通對象,則對象頭是12個字節(jié)
但是請注意,對于 new Object() 來說,首先它不是數(shù)組,則對象頭為12個字節(jié),其次它是一個空對象,則它的實例數(shù)據(jù)為0,但是一個對象所占的字節(jié)必須是8個字節(jié)的整數(shù)倍,所以對齊填充位要補4個字節(jié)!加起來一共是16個字節(jié)!
我們可以用JOL來佐證!詳情如下:?
public class ObjectTest {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
四、使用JOL工具查看內(nèi)存布局查看普通 java對象的內(nèi)部布局工具JOL(JAVA OBJECT LAYOUT),使用此工具可以查看 new 出來的一個 java對象的內(nèi)部布局,以及一個普通的java對象占用多少字節(jié)。?
導(dǎo)pom依賴org.openjdk.jol jol-core0.10
示例代碼?public class ObjectTest {
public static void main(String[] args) throws InterruptedException {
Object obj = new Test();
//查看對象內(nèi)部信息
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
class Test{
private boolean flag;
private long p;
}
運行結(jié)果?下一節(jié)——synchronized底層鎖的優(yōu)化解析你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級服務(wù)器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧