您現在的位置是:首頁 > 手機遊戲首頁手機遊戲

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

  • 由 黑馬程式設計師 發表于 手機遊戲
  • 2021-12-01
簡介monitorenter小結:synchronized的鎖物件會關聯一個monitor,這個monitor不是我們主動建立的,是JVM的執行緒執行到這個 同步程式碼塊,發現鎖物件沒有monitor就會建立monitor,monitor內部有

associated怎麼讀

第四章:synchronized的特性

可重入特性

瞭解什麼是可重入

瞭解可重入的原理

什麼是可重入

一個執行緒可以多次執行synchronized,重複獲取同一把鎖。

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

可重入原理

synchronized的鎖物件中有一個計數器(recursions變數)會記錄執行緒獲得幾次鎖。

可重入的好處

1。 可以避免死鎖

2。 可以讓我們更好的來封裝程式碼

小結

synchronized是可重入鎖,內部鎖物件中會有一個計數器記錄執行緒獲取幾次鎖啦,在執行完同步程式碼塊 時,計數器的數量會-1,知道計數器的數量為0,就釋放這個鎖。

不可中斷特性

學習synchronized不可中斷特性

學習Lock的可中斷特性

什麼是不可中斷

一個執行緒獲得鎖後,另一個執行緒想要獲得鎖,必須處於阻塞或等待狀態,如果第一個執行緒不釋放鎖,第二個執行緒會一直阻塞或等待,不可被中斷。

synchronized不可中斷演示

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

synchronized是不可中斷,處於阻塞狀態的執行緒會一直等待鎖。

ReentrantLock可中斷演示

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

小結

不可中斷是指,當一個執行緒獲得鎖後,另一個執行緒一直處於阻塞或等待狀態,前一個執行緒不釋放鎖,後 一個執行緒會一直阻塞或等待,不可被中斷。

synchronized屬於不可被中斷

Lock的lock方法是不可中斷的

Lock的tryLock方法是可中斷的

第五章:synchronized原理

javap 反彙編

透過javap反彙編學習synchronized的原理

我們編寫一個簡單的synchronized程式碼,如下:

package com。itheima。demo04_synchronized_monitor; public class Demo01 { private static Object obj = new Object(); public static void main(String[] args) { synchronized (obj) {System。out。println(“1”); } }public synchronized void test() { System。out。println(“a”); } }

我們要看synchronized的原理,但是synchronized是一個關鍵字,看不到原始碼。我們可以將class檔案 進行反彙編。

JDK自帶的一個工具: javap ,對位元組碼進行反彙編,檢視位元組碼指令。

在DOS命令列輸入:

javap -p -v -c C:\Users\13666\IdeaProjects\HeiMa\Synchronized\target\classes\com\itheima\demo04 _synchronized_monitor\Increment。class

反彙編後的效果如下:

public static void main(java。lang。String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: iconst_0 1: istore_1 2: getstatic #2 // Field obj:Ljava/lang/Object; 5: dup 6: astore_2 7: monitorenter 8: iinc 1, 1 11: aload_2 12: monitorexit 13: goto 21 16: astore_3 17: aload_2 18: monitorexit 19: aload_3 20: athrow 21: return Exception table: from to target type 8 13 16 any 16 19 16 any LineNumberTable: line 8: 0 line 9: 2 line 10: 8 line 11: 11 line 12: 21 LocalVariableTable: Start Length Slot Name Signaturemonitorenter 0 22 0 args [Ljava/lang/String; 2 20 1 number I StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 16 locals = [ class “[Ljava/lang/String;”, int, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 public synchronized void test(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #3 // Field java/lang/System。out:Ljava/io/PrintStream; 3: ldc #4 // String a 5: invokevirtual #5 // Method java/io/PrintStream。println:(Ljava/lang/String;)V 8: return LineNumberTable: line 15: 0 line 16: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 this Lcom/itheima/demo04_synchronized_monitor/Demo01;

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

monitorenter

首先我們來看一下JVM規範中對於monitorenter的描述:

Each object is associated with a monitor。 A monitor is locked if and only if it has an owner。 The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows: If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one。 The thread is then the owner of the monitor。 If the thread already owns the monitor associated with objectref, it reenters the monitor,incrementing its entry count。 If another thread

翻譯過來: 每一個物件都會和一個監視器monitor關聯。監視器被佔用時會被鎖住,其他執行緒無法來獲 取該monitor。 當JVM執行某個執行緒的某個方法內部的monitorenter時,它會嘗試去獲取當前物件對應 的monitor的所有權。其過程如下:

1。 若monior的進入數為0,執行緒可以進入monitor,並將monitor的進入數置為1。當前執行緒成為 monitor的owner(所有者)

2。 若執行緒已擁有monitor的所有權,允許它重入monitor,則進入monitor的進入數加1

3。 若其他執行緒已經佔有monitor的所有權,那麼當前嘗試獲取monitor的所有權的執行緒會被阻塞,直到monitor的進入數變為0,才能重新嘗試獲取monitor的所有權。

monitorenter小結

synchronized的鎖物件會關聯一個monitor,這個monitor不是我們主動建立的,是JVM的執行緒執行到這個 同步程式碼塊,發現鎖物件沒有monitor就會建立monitor,monitor內部有兩個重要的成員變數owner:擁有 這把鎖的執行緒,recursions會記錄執行緒擁有鎖的次數,當一個執行緒擁有monitor後其他執行緒只能等待

monitorexit

首先我們來看一下JVM規範中對於monitorexit的描述:

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref。 The thread decrements the entry count of the monitor associated with objectref。 If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner。 Other threads that are blocking to enter the monitor are allowed to attempt to do so。

翻譯過來:

1。 能執行monitorexit指令的執行緒一定是擁有當前物件的monitor的所有權的執行緒。

2。 執行monitorexit時會將monitor的進入數減1。當monitor的進入數減為0時,當前執行緒退出

monitor,不再擁有monitor的所有權,此時其他被這個monitor阻塞的執行緒可以嘗試去獲取這個 monitor的所有權

monitorexit釋放鎖。

monitorexit插入在方法結束處和異常處,JVM保證每個monitorenter必須有對應的monitorexit。

面試題synchroznied出現異常會釋放鎖嗎?

會釋放鎖

同步方法

可以看到同步方法在反彙編後,會增加 ACC_SYNCHRONIZED 修飾。會隱式呼叫monitorenter和 monitorexit。在執行同步方法前會呼叫monitorenter,在執行完同步方法後會呼叫monitorexit。

小結

透過javap反彙編我們看到synchronized使用程式設計了monitorentor和monitorexit兩個指令。每個鎖物件 都會關聯一個monitor(監視器,它才是真正的鎖物件),它內部有兩個重要的成員變數owner會儲存獲得鎖 的執行緒,recursions會儲存執行緒獲得鎖的次數,當執行到monitorexit時,recursions會-1,當計數器減到0時這個執行緒就會釋放鎖

面試題:synchronized與Lock的區別

1。 synchronized是關鍵字,而Lock是一個介面。

2。 synchronized會自動釋放鎖,而Lock必須手動釋放鎖。

3。 synchronized是不可中斷的,Lock可以中斷也可以不中斷。

4。 透過Lock可以知道執行緒有沒有拿到鎖,而synchronized不能。

5。 synchronized能鎖住方法和程式碼塊,而Lock只能鎖住程式碼塊。

6。 Lock可以使用讀鎖提高多執行緒讀效率。

7。 synchronized是非公平鎖,ReentrantLock可以控制是否是公平鎖。

深入JVM原始碼

透過JVM原始碼分析synchronized的原理

monitor監視器鎖

可以看出無論是synchronized程式碼塊還是synchronized方法,其執行緒安全的語義實現最終依賴一個叫 monitor的東西,那麼這個神秘的東西是什麼呢?下面讓我們來詳細介紹一下。

在HotSpot虛擬機器中,monitor是由ObjectMonitor實現的。其原始碼是用c++來實現的,位於HotSpot虛 擬機原始碼ObjectMonitor。hpp檔案中(src/share/vm/runtime/objectMonitor。hpp)。

ObjectMonitor主 要資料結構如下:

ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; // 執行緒的重入次數_object = NULL; // 儲存該monitor的物件 _owner = NULL; // 標識擁有該monitor的執行緒 _WaitSet = NULL; // 處於wait狀態的執行緒,會被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL; _succ = NULL; _cxq = NULL; // 多執行緒競爭鎖時的單向列表 FreeNext = NULL; _EntryList = NULL; // 處於等待鎖block狀態的執行緒,會被加入到該列表 _SpinFreq = 0; _SpinClock = 0; OwnerIsThread = 0; }

1。 _owner:初始時為NULL。當有執行緒佔有該monitor時,owner標記為該執行緒的唯一標識。當執行緒 釋放monitor時,owner又恢復為NULL。owner是一個臨界資源,JVM是透過CAS操作來保證其線 程安全的。

2。 _cxq:競爭佇列,所有請求鎖的執行緒首先會被放在這個佇列中(單向連結)。_cxq是一個臨界資 源,JVM透過CAS原子指令來修改_cxq佇列。修改前_cxq的舊值填入了node的next欄位,_cxq指向新值(新執行緒)。因此_cxq是一個後進先出的stack(棧)。

3。 _EntryList:_cxq佇列中有資格成為候選資源的執行緒會被移動到該佇列中。

4。 _WaitSet:因為呼叫wait方法而被阻塞的執行緒會被放在該佇列中。

每一個Java物件都可以與一個監視器monitor關聯,我們可以把它理解成為一把鎖,當一個執行緒想要執 行一段被synchronized圈起來的同步方法或者程式碼塊時,該執行緒得先獲取到synchronized修飾的物件對應的monitor。 我們的Java程式碼裡不會顯示地去創造這麼一個monitor物件,我們也無需建立,事實上可以這麼理解:

monitor並不是隨著物件建立而建立的。我們是透過synchronized修飾符告訴JVM需要為我們的某個物件建立關聯的monitor物件。每個執行緒都存在兩個ObjectMonitor物件列表,分別為free和used列表。 同時JVM中也維護著global locklist。當執行緒需要ObjectMonitor物件時,首先從執行緒自身的free表中申請,若存在則使用,若不存在則從global list中申請。

ObjectMonitor的資料結構中包含:_owner、_WaitSet和_EntryList,它們之間的關係轉換可以用下圖

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

表示:

monitor競爭

1。 執行monitorenter時,會呼叫InterpreterRuntime。cpp

(位於:src/share/vm/interpreter/interpreterRuntime。cpp) 的 InterpreterRuntime::monitorenter函 數。具體程式碼可參見HotSpot原始碼。

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)) #ifdef ASSERT thread->last_frame()。interpreter_frame_verify_monitor(elem); #endif if (PrintBiasedLockingStatistics) { Atomic::inc(BiasedLocking::slow_path_entry_count_addr()); }Handle h_obj(thread, elem->obj()); assert(Universe::heap()->is_in_reserved_or_null(h_obj()), “must be NULL or an object”); if (UseBiasedLocking) { // Retry fast entry if bias is revoked to avoid unnecessary inflation ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK); } else { ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); }assert(Universe::heap()->is_in_reserved_or_null(elem->obj()), “must be NULL or an object”);

2。對於重量級鎖,monitorenter函式中會呼叫 ObjectSynchronizer::slow_enter

3。最終呼叫 ObjectMonitor::enter(位於:src/share/vm/runtime/objectMonitor。cpp),原始碼如下:

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

此處省略鎖的自旋最佳化等操作,統一放在後面synchronzied最佳化中說。

以上程式碼的具體流程概括如下:

1。 透過CAS嘗試把monitor的owner欄位設定為當前執行緒。

2。 如果設定之前的owner指向當前執行緒,說明當前執行緒再次進入monitor,即重入鎖,執行

recursions ++ ,記錄重入的次數。

3。 如果當前執行緒是第一次進入該monitor,設定recursions為1,_owner為當前執行緒,該執行緒成功獲得鎖並返回。

4。 如果獲取鎖失敗,則等待鎖的釋放。

monitor等待

競爭失敗等待呼叫的是ObjectMonitor物件的EnterI方法(位於:src/share/vm/runtime/objectMonitor。cpp),原始碼如下所示:

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

當該執行緒被喚醒時,會從掛起的點繼續執行,透過 ObjectMonitor::TryLock 嘗試獲取鎖,TryLock方 法實現如下:

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

以上程式碼的具體流程概括如下:

1。 當前執行緒被封裝成ObjectWaiter物件node,狀態設定成ObjectWaiter::TS_CXQ。

2。 在for迴圈中,透過CAS把node節點push到_cxq列表中,同一時刻可能有多個執行緒把自己的node節點push到_cxq列表中。

3。 node節點push到_cxq列表之後,透過自旋嘗試獲取鎖,如果還是沒有獲取到鎖,則透過park將當前執行緒掛起,等待被喚醒。

4。 當該執行緒被喚醒時,會從掛起的點繼續執行,透過 ObjectMonitor::TryLock 嘗試獲取鎖。

monitor釋放

當某個持有鎖的執行緒執行完同步程式碼塊時,會進行鎖的釋放,給其它執行緒機會執行同步程式碼,在HotSpot中,透過退出monitor的方式實現鎖的釋放,並通知被阻塞的執行緒,具體實現位於 ObjectMonitor的exit方法中。(位於:src/share/vm/runtime/objectMonitor。cpp),原始碼如下所示:

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

1。 退出同步程式碼塊時會讓_recursions減1,當_recursions的值減為0時,說明執行緒釋放了鎖。

2。 根據不同的策略(由QMode指定),從cxq或EntryList中獲取頭節點,透過

ObjectMonitor::ExitEpilog 方法喚醒該節點封裝的執行緒,喚醒操作最終由unpark完成,實現

如下:

void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) { assert (_owner == Self, “invariant”) ; _succ = Knob_SuccEnabled ? Wakee->_thread : NULL ; ParkEvent * Trigger = Wakee->_event ; Wakee = NULL ; // Drop the lock OrderAccess::release_store_ptr (&_owner, NULL) ; OrderAccess::fence() ; // ST _owner vs LD in unpark() if (SafepointSynchronize::do_call_back()) { TEVENT (unpark before SAFEPOINT) ; }DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self); Trigger->unpark() ; // 喚醒之前被pack()掛起的執行緒。 // Maintain stats and report events to JVMTI if (ObjectMonitor::_sync_Parks != NULL) { ObjectMonitor::_sync_Parks->inc() ; } }

被喚醒的執行緒,會回到 void ATTR ObjectMonitor::EnterI (TRAPS) 的第600行,繼續執行monitor的競爭。

// park self if (_Responsible == Self || (SyncFlags & 1)) { TEVENT (Inflated enter - park TIMED) ; Self->_ParkEvent->park ((jlong) RecheckInterval) ; // Increase the RecheckInterval, but clamp the value。 RecheckInterval *= 8 ; if (RecheckInterval > 1000) RecheckInterval = 1000 ; } else { TEVENT (Inflated enter - park UNTIMED) ; Self->_ParkEvent->park() ; }if (TryLock(Self) > 0) break ;

monitor是重量級鎖

可以看到ObjectMonitor的函式呼叫中會涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等核心函式,執行同步程式碼塊,沒有競爭到鎖的物件會park()被掛起,競爭到鎖的執行緒會unpark()喚醒。這個時候就會存在作業系統使用者態和核心態的轉換,這種切換會消耗大量的系統資源。所以synchronized是Java語言中是一個重量級(Heavyweight)的操作。

使用者態和和核心態是什麼東西呢?要想了解使用者態和核心態還需要先了解一下Linux系統的體系架構:

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

從上圖可以看出,Linux作業系統的體系架構分為:使用者空間(應用程式的活動空間)和核心。

核心:本質上可以理解為一種軟體,控制計算機的硬體資源,並提供上層應用程式執行的環境。

使用者空間:上層應用程式活動的空間。應用程式的執行必須依託於核心提供的資源,包括CPU資源、儲存資源、I/O資源等。

系統呼叫:為了使上層應用能夠訪問到這些資源,核心必須為上層應用提供訪問的介面:即系統呼叫。

所有程序初始都運行於使用者空間,此時即為使用者執行狀態(簡稱:使用者態);但是當它呼叫系統呼叫執 行某些操作時,例如 I/O呼叫,此時需要陷入核心中執行,我們就稱程序處於核心執行態(或簡稱為內 核態)。 系統呼叫的過程可以簡單理解為:

1。 使用者態程式將一些資料值放在暫存器中, 或者使用引數建立一個堆疊, 以此表明需要作業系統提供的服務。

2。 使用者態程式執行系統呼叫。

3。 CPU切換到核心態,並跳到位於記憶體指定位置的指令。

4。 系統呼叫處理器(system call handler)會讀取程式放入記憶體的資料引數,並執行程式請求的服務。

5。 系統呼叫完成後,作業系統會重置CPU為使用者態並返回系統呼叫的結果。

由此可見使用者態切換至核心態需要傳遞許多變數,同時核心還需要保護好使用者態在切換時的一些暫存器值、變數等,以備核心態切換回使用者態。這種切換就帶來了大量的系統資源消耗,這就是在synchronized未最佳化之前,效率低的原因。

第六章:JDK6 synchronized最佳化

CAS

學習CAS的作用

學習CAS的原理

CAS概述和作用

CAS的全成是: Compare And Swap(比較相同再交換)。是現代CPU廣泛支援的一種對記憶體中的共享資料進行操作的一種特殊指令。

CAS的作用:CAS可以將比較和交換轉換為原子操作,這個原子操作直接由CPU保證。CAS可以保證共享變數賦值時的原子操作。CAS操作依賴3個值:記憶體中的值V,舊的預估值X,要修改的新值B,如果舊的預估值X等於記憶體中的值V,就將新的值B儲存到記憶體中。

CAS和volatile實現無鎖併發

package com。itheima。demo05_cas; import java。util。ArrayList; import java。util。concurrent。atomic。AtomicInteger; public class Demo01 { public static void main(String[] args) throws InterruptedException { AtomicInteger atomicInteger = new AtomicInteger(); Runnable mr = () -> { for (int i = 0; i < 1000; i++) { atomicInteger。incrementAndGet(); } };ArrayList ts = new ArrayList<>(); for (int i = 0; i < 5; i++) { Thread t = new Thread(mr);

CAS原理

透過剛才AtomicInteger的原始碼我們可以看到,Unsafe類提供了原子操作。

Unsafe類介紹

Unsafe類使Java擁有了像C語言的指標一樣操作記憶體空間的能力,同時也帶來了指標的問題。過度的使用Unsafe類會使得出錯的機率變大,因此Java官方並不建議使用的,官方文件也幾乎沒有。Unsafe物件不能直接呼叫,只能透過反射獲得。

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

Unsafe實現CAS

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

樂觀鎖和悲觀鎖

悲觀鎖

從悲觀的角度出發:

總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞。因此synchronized我們也將其稱之為

悲觀鎖

。JDK中的ReentrantLock也是一種悲觀鎖。效能較差!

樂觀鎖

從樂觀的角度出發:

總是假設最好的情況,每次去拿資料的時候都認為別人不會修改,就算改了也沒關係,再重試即可。所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去修改這個資料,如何沒有人修改則更新,如果有人修改則重試。

CAS這種機制我們也可以將其稱之為樂觀鎖。綜合性能較好!

CAS獲取共享變數時,為了保證該變數的可見性,需要使用volatile修飾。結合CAS和volatile可以實現無鎖併發,適用於競爭不激烈、多核 CPU 的場景下。

1。 因為沒有使用 synchronized,所以執行緒不會陷入阻塞,這是效率提升的因素之一。

2。 但如果競爭激烈,可以想到重試必然頻繁發生,反而效率會受影響。

小結

CAS的作用? Compare And Swap,CAS可以將比較和交換轉換為原子操作,這個原子操作直接由處理器保證。

CAS的原理?CAS需要3個值:記憶體地址V,舊的預期值A,要修改的新值B,如果記憶體地址V和舊的預期值A相等就修改記憶體地址值為B

synchronized鎖升級過程

高效併發是從JDK 5到JDK 6的一個重要改進,HotSpot虛擬機器開發團隊在這個版本上花費了大量的精力去實現各種鎖最佳化技術,包括偏向鎖( Biased Locking )、輕量級鎖( Lightweight Locking )和如適應性自旋(Adaptive Spinning)、鎖消除( Lock Elimination)、鎖粗化( Lock Coarsening )等,這些技術都是為了線上程之間更高效地共享資料,以及解決競爭問題,從而提高程式的執行效率。

無鎖——》偏向鎖——》輕量級鎖–》重量級鎖

Java物件的佈局

學習Java物件的佈局

在JVM中,物件在記憶體中的佈局分為三塊區域:物件頭、例項資料和對齊填充。如下圖所示:

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

物件頭

當一個執行緒嘗試訪問synchronized修飾的程式碼塊時,它首先要獲得鎖,那麼這個鎖到底存在哪裡呢?是存在鎖物件的物件頭中的。

HotSpot採用instanceOopDesc和arrayOopDesc來描述物件頭,arrayOopDesc物件用來描述陣列型別。instanceOopDesc的定義的在Hotspot原始碼的 instanceOop。hpp 檔案中,另外,arrayOopDesc的定義對應 arrayOop。hpp 。

class instanceOopDesc : public oopDesc { public: // aligned header size。 static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; } // If compressed, the offset of the fields of the instance may not be aligned。 static int base_offset_in_bytes() { // offset computation code breaks if UseCompressedClassPointers // only is true return (UseCompressedOops && UseCompressedClassPointers) ? klass_gap_offset_in_bytes() : sizeof(instanceOopDesc); }static bool contains_field_offset(int offset, int nonstatic_field_size) { int base_in_bytes = base_offset_in_bytes(); return (offset >= base_in_bytes && (offset-base_in_bytes) < nonstatic_field_size * heapOopSize); } };

從instanceOopDesc程式碼中可以看到 instanceOopDesc繼承自oopDesc,oopDesc的定義載Hotspot原始碼中的 oop。hpp 檔案中。

class oopDesc { friend class VMStructs; private: volatile markOop _mark; union _metadata { Klass* _klass; narrowKlass _compressed_klass; } _metadata; // Fast access to barrier set。 Must be initialized。 static BarrierSet* _bs; // 省略其他程式碼 };

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

在普通例項物件中,oopDesc的定義包含兩個成員,分別是 _mark 和 _metadata

_mark 表示物件標記、屬於markOop型別,也就是接下來要講解的Mark World,它記錄了物件和鎖有關的資訊

_metadata 表示類元資訊,類元資訊儲存的是物件指向它的類元資料(Klass)的首地址,其中Klass表示普通指標、 _compressed_klass 表示壓縮類指標。

物件頭由兩部分組成,一部分用於儲存自身的執行時資料,稱之為 Mark Word,另外一部分是型別指標,及物件指向它的類元資料的指標。

Mark Word

Mark Word用於儲存物件自身的執行時資料,如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等等,佔用記憶體大小與虛擬機器位長一致。Mark Word對應的型別是 markOop 。原始碼位於 markOop。hpp 中。

class oopDesc { friend class VMStructs; private: volatile markOop _mark; union _metadata { Klass* _klass; narrowKlass _compressed_klass; } _metadata; // Fast access to barrier set。 Must be initialized。 static BarrierSet* _bs; // 省略其他程式碼 };// Bit-format of an object header (most significant first, big endian layout below): //// 32 bits: // ———— // hash:25 ——————>| age:4 biased_lock:1 lock:2 (normal object)// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object) // size:32 ——————————————————————>| (CMS free block) // PromotedObject*:29 ——————>| promo_bits:3 ——->| (CMS promoted object) //// 64 bits: // ———— // unused:25 hash:31 ——>| unused:1 age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object) // PromotedObject*:61 ——————————->| promo_bits:3 ——->| (CMS promoted object) // size:64 ——————————————————————————->| (CMS free block) // [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread // [0 | epoch | age | 1 | 01] lock is anonymously biased //// - the two lock bits are used to describe three states: locked/unlocked and monitor。 //// [ptr | 00] locked ptr points to real header on stack // [header | 0 | 01] unlocked regular object header // [ptr | 10] monitor inflated lock (header is wapped out) // [ptr | 11] marked used by markSweep to mark an object // not valid at any other time

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

在64位虛擬機器下,Mark Word是64bit大小的,其儲存結構如下:在32位虛擬機器下,

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

Mark Word是32bit大小的,其儲存結構如下:

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

klass pointer

這一部分用於儲存物件的型別指標,該指標指向它的類元資料,JVM透過這個指標確定物件是哪個類的例項。該指標的位長度為JVM的一個字大小,即32位的JVM為32位,64位的JVM為64位。 如果應用的物件過多,使用64位的指標將浪費大量記憶體,統計而言,64位的JVM將會比32位的JVM多耗費50%的記憶體。為了節約記憶體可以使用選項 -XX:+UseCompressedOops 開啟指標壓縮,其中,oop即ordinary object pointer普通物件指標。開啟該選項後,下列指標將壓縮至32位:

1。 每個Class的屬性指標(即靜態變數)

2。 每個物件的屬性指標(即物件變數)

3。 普通物件陣列的每個元素指標

當然,也不是所有的指標都會壓縮,一些特殊型別的指標JVM不會最佳化,比如指向PermGen的Class物件指標(JDK8中指向元空間的Class物件指標)、本地變數、堆疊元素、入參、返回值和NULL指標等。

物件頭 = Mark Word + 型別指標(未開啟指標壓縮的情況下)

在32位系統中,Mark Word = 4 bytes,型別指標 = 4bytes,物件頭 = 8 bytes = 64 bits;在64位系統中,Mark Word = 8 bytes,型別指標 = 8bytes,物件頭 = 16 bytes = 128bits;

例項資料

就是類中定義的成員變數。

對齊填充

對齊填充並不是必然存在的,也沒有什麼特別的意義,他僅僅起著佔位符的作用,由於HotSpot VM的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,換句話說,就是物件的大小必須是8位元組的整數倍。而物件頭正好是8位元組的倍數,因此,當物件例項資料部分沒有對齊時,就需要透過對齊填充來補全。

檢視Java物件佈局

org。openjdk。jol jol-core 0。9 };

小結

Java物件由3部分組成,物件頭,例項資料,對齊資料

物件頭分成兩部分:Mark World + Klass pointer

偏向鎖

學習偏向鎖的原理和好處

什麼是偏向鎖

偏向鎖是JDK 6中的重要引進,因為HotSpot作者經過研究實踐發現,在大多數情況下,鎖不僅不存在多 執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低,引進了偏向鎖。

偏向鎖的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是這個鎖會偏向於第一個獲得它的執行緒,會在物件頭儲存鎖偏向的執行緒ID,以後該執行緒進入和退出同步塊時只需要檢查是否為偏向鎖、鎖標誌位以及ThreadID即可。

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

不過一旦出現多個執行緒競爭時必須撤銷偏向鎖,所以撤銷偏向鎖消耗的效能必須小於之前節省下來的 CAS原子操作的效能消耗,不然就得不償失了。

偏向鎖原理

當執行緒第一次訪問同步塊並獲取鎖時,偏向鎖處理流程如下:

1。 虛擬機器將會把物件頭中的標誌位設為“01”,即偏向模式。

2。 同時使用CAS操作把獲取到這個鎖的執行緒的ID記錄在物件的Mark Word之中 ,如果CAS操作成功,持有偏向鎖的執行緒以後每次進入這個鎖相關的同步塊時,虛擬機器都可以不再進行任何 同步操作,偏向鎖的效率高。

持有偏向鎖的執行緒以後每次進入這個鎖相關的同步塊時,虛擬機器都可以不再進行任何同步操作,偏向鎖的效率高。

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

偏向鎖的撤銷

1。 偏向鎖的撤銷動作必須等待全域性安全點

2。 暫停擁有偏向鎖的執行緒,判斷鎖物件是否處於被鎖定狀態

3。 撤銷偏向鎖,恢復到無鎖(標誌位為

01

)或輕量級鎖(標誌位為

00

)的狀態偏向鎖在Java 6之後是預設啟用的,但在應用程式啟動幾秒鐘之後才啟用,可以使用 -XX:BiasedLockingStartupDelay=0 引數關閉延遲,如果確定應用程式中所有鎖通常情況下處於競爭 狀態,可以透過 XX:-UseBiasedLocking=false 引數關閉偏向鎖。

偏向鎖好處

偏向鎖是在只有一個執行緒執行同步塊時進一步提高效能,適用於一個執行緒反覆獲得同一鎖的情況。偏向鎖可以提高帶有同步但無競爭的程式效能。 它同樣是一個帶有效益權衡性質的最佳化,也就是說,它並不一定總是對程式執行有利,如果程式中大多數的鎖總是被多個不同的執行緒訪問比如執行緒池,那偏向模式就是多餘的。

在JDK5中偏向鎖預設是關閉的,而到了JDK6中偏向鎖已經預設開啟。但在應用程式啟動幾秒鐘之後才啟用,可以使用 -XX:BiasedLockingStartupDelay=0 引數關閉延遲,如果確定應用程式中所有鎖通常情況下處於競爭狀態,可以透過 XX:-UseBiasedLocking=false 引數關閉偏向鎖。

小結

偏向鎖的原理是什麼?

當鎖物件第一次被執行緒獲取的時候,虛擬機器將會把物件頭中的標誌位設為“01”,即偏向模式。同時使用CAS操 作把獲取到這個鎖的執行緒的ID記錄在物件的Mark Word之中 ,如果CAS操作成功,持有偏向鎖的執行緒以後每次進入這個鎖相關的同步塊時,虛擬機器都可以不再進行任何同步操作,偏向鎖的效率高。

偏向鎖的好處是什麼?

偏向鎖是在只有一個執行緒執行同步塊時進一步提高效能,適用於一個執行緒反覆獲得同一鎖的情況。偏向鎖可以提高帶有同步但無競爭的程式效能。

輕量級鎖

學習輕量級鎖的原理和好處

什麼是輕量級鎖

輕量級鎖是JDK 6之中加入的新型鎖機制,它名字中的“輕量級”是相對於使用monitor的傳統鎖而言的,因此傳統的鎖機制就稱為“重量級”鎖。首先需要強調一點的是,輕量級鎖並不是用來代替重量級鎖的。引入輕量級鎖的目的:在多執行緒交替執行同步塊的情況下,儘量避免重量級鎖引起的效能消耗,但是如果多個執行緒在同一時刻進入臨界區,會導致輕量級鎖膨脹升級重量級鎖,所以輕量級鎖的出現並非是要替代重量級鎖。

輕量級鎖原理

當關閉偏向鎖功能或者多個執行緒競爭偏向鎖導致偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖,其步驟如下: 獲取鎖

1。 判斷當前物件是否處於無鎖狀態(hashcode、0、01),如果是,則JVM首先將在當前執行緒的棧幀 中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的複製(官方 把這份複製加了一個Displaced字首,即Displaced Mark Word),將物件的Mark Word複製到棧 幀中的Lock Record中,將Lock Reocrd中的owner指向當前物件。

2。 JVM利用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指標,如果成功表示競爭到鎖,則將鎖標誌位變成00,執行同步操作。

3。 如果失敗則判斷當前物件的Mark Word是否指向當前執行緒的棧幀,如果是則表示當前執行緒已經持有當前物件的鎖,則直接執行同步程式碼塊;否則只能說明該鎖物件已經被其他執行緒搶佔了,這時輕量級鎖需要膨脹為重量級鎖,鎖標誌位變成10,後面等待的執行緒將會進入阻塞狀態。

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

輕量級鎖的釋放

輕量級鎖的釋放也是透過CAS操作來進行的,主要步驟如下:

1。 取出在獲取輕量級鎖儲存在Displaced Mark Word中的資料。

2。 用CAS操作將取出的資料替換當前物件的Mark Word中,如果成功,則說明釋放鎖成功。3。 如果CAS操作替換失敗,說明有其他執行緒嘗試獲取該鎖,則需要將輕量級鎖需要膨脹升級為重量級鎖。

對於輕量級鎖,其效能提升的依據是“對於絕大部分的鎖,在整個生命週期內都是不會存在競爭的”,如 果打破這個依據則除了互斥的開銷外,還有額外的CAS操作,因此在有多執行緒競爭的情況下,輕量級鎖比重量級鎖更慢。

輕量級鎖好處

在多執行緒交替執行同步塊的情況下,可以避免重量級鎖引起的效能消耗。

小結

輕量級鎖的原理是什麼?

將物件的Mark Word複製到棧幀中的Lock Recod中。Mark Word更新為指向Lock Record的指標。

輕量級鎖好處是什麼?

在多執行緒交替執行同步塊的情況下,可以避免重量級鎖引起的效能消耗。

自旋鎖

學習自旋鎖原理

自旋鎖原理

synchronized (Demo01。class) { 。。。 System。out。println(“aaa”); }

前面我們討論monitor實現鎖的時候,知道monitor會阻塞和喚醒執行緒,執行緒的阻塞和喚醒需要CPU從使用者態轉為核心態,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作,這些操作給系統的併發效能帶來了很大的壓力。同時,虛擬機器的開發團隊也注意到在許多應用上,共享資料的鎖定狀態只會持續很短的一段時間,為了這段時間阻塞和喚醒執行緒並不值得。如果物理機器有一個以上的處理器,能讓兩個或以上的執行緒同時並行執行,我們就可以讓後面請求鎖的那個執行緒“稍等一下”,但不放棄處理器的執行時間,看看持有鎖的執行緒是否很快就會釋放鎖。為了讓執行緒等待,我們只需讓執行緒執行一個忙迴圈(自旋) , 這項技術就是所謂的自旋鎖。

自旋鎖在JDK 1。4。2中就已經引入 ,只不過預設是關閉的,可以使用-XX:+UseSpinning引數來開啟,在JDK 6中 就已經改為預設開啟了。自旋等待不能代替阻塞,且先不說對處理器數量的要求,自旋等待本身雖然避免了執行緒切換的開銷,但它是要佔用處理器時間的,因此,如果鎖被佔用的時間很短,自旋等待的效果就會非常好,反之,如果鎖被佔用的時間很長。那麼自旋的執行緒只會白白消耗處理器資源,而不會做任何有用的工作,反而會帶來性 能上的浪費。因此,自旋等待的時間必須要有一定的限度,如果在多執行緒交替執行同步塊的情況下,可以避免重量級鎖引起的效能消耗。

自旋超過了限定的次數仍然沒有成功獲得鎖,就應當使用傳統的方式去掛起執行緒了。自旋次數的預設值是10次,使用者可以使用引數-XX : PreBlockSpin來更改。

適應性自旋鎖

在JDK 6中引入了自適應的自旋鎖。自適應意味著自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖物件上,自旋等待剛剛成功獲得過鎖,並且持有鎖的執行緒正在執行中,那麼虛擬機器就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長的時間,比如100次迴圈。另外,如果對於某個鎖,自旋很少成功獲得過,那在以後要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。有了自適應自旋,隨著程式執行和效能監控資訊的不斷完善,虛擬機器對程式鎖的狀況預測就會越來越準確,虛擬機器就會變得越來越“聰明”了。

鎖消除

學習如何進行鎖消除

鎖消除是指虛擬機器即時編譯器(JIT)在執行時,對一些程式碼上要求同步,但是被檢測到不可能存在共享 資料競爭的鎖進行消除。鎖消除的主要判定依據來源於逃逸分析的資料支援,如果判斷在一段程式碼中,堆上的所有資料都不會逃逸出去從而被其他執行緒訪問到,那就可以把它們當做棧上資料對待,認為它們是執行緒私有的,同步加鎖自然就無須進行。變數是否逃逸,對於虛擬機器來說需要使用資料流分析來確定,但是程式設計師自己應該是很清楚的,怎麼會在明知道不存在資料爭用的情況下要求同步呢?實際上有許多同步措施並不是程式設計師自己加入的,同步的程式碼在Java程式中的普遍程度也許超過了大部分讀者的想象。

下面這段非常簡單的程式碼僅僅是輸出3個字串相加的結果,無論是原始碼字面上還是程式語義上都沒有同步。

public class Demo01 { public static void main(String[] args) { contactString(“aa”, “bb”, “cc”); }public static String contactString(String s1, String s2, String s3) { return new StringBuffer()。append(s1)。append(s2)。append(s3)。toString(); } }

StringBuffffer的append ( ) 是一個同步方法,鎖就是this也就是(new StringBuilder())。虛擬機發現它的動態作用域被限制在concatString( )方法內部。也就是說, new StringBuilder()物件的引用永遠不會“逃逸”到concatString ( )方法之外,其他執行緒無法訪問到它,因此,雖然這裡有鎖,但是可以被安全地消除掉,在即時編譯之後,這段程式碼就會忽略掉所有的同步而直接執行了。

鎖粗化

學習鎖粗化的原理

public class Demo01 { public static void main(String[] args) { contactString(“aa”, “bb”, “cc”); }public static String contactString(String s1, String s2, String s3) { return new StringBuffer()。append(s1)。append(s2)。append(s3)。toString(); } }

原則上,我們在編寫程式碼的時候,總是推薦將同步塊的作用範圍限制得儘量小,只在共享資料的實際作用域中才進行同步,這樣是為了使得需要同步的運算元量儘可能變小,如果存在鎖競爭,那等待鎖的執行緒也能儘快拿到鎖。大部分情況下,上面的原則都是正確的,但是如果一系列的連續操作都對同一個物件反覆加鎖和解鎖,甚至加鎖操作是出現在迴圈體中的,那即使沒有執行緒競爭,頻繁地進行互斥同步操作也會導致不必要的效能損耗。

小結

什麼是鎖粗化?JVM會探測到一連串細小的操作都使用同一個物件加鎖,將同步程式碼塊的範圍放大,放到這串操作的外面,這樣只需要加一次鎖即可。

平時寫程式碼如何對synchronized最佳化

減少synchronized的範圍

同步程式碼塊中儘量短,減少同步程式碼塊中程式碼的執行時間,減少鎖的競爭。

synchronized (Demo01。class) { System。out。println(“aaa”); }

降低synchronized鎖的粒度

將一個鎖拆分為多個鎖提高併發度

Hashtable hs = new Hashtable(); hs。put(“aa”, “bb”); hs。put(“xx”, “yy”);

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

LinkedBlockingQueue入隊和出隊使用不同的鎖,相對於讀寫只有一個鎖效率要高

Java面試熱點:深入學習併發程式設計中的synchronized(後三章)

讀取時不加鎖,寫入和刪除時加鎖

ConcurrentHashMap,CopyOnWriteArrayList和ConyOnWriteSet

Top