synchronized
把面試中遇到的問題進行了整理. 本篇文章copy+整理自:
1. http://www.cnblogs.com/lingepeiyong/archive/2012/10/30/2745973.html
2. http://www.cnblogs.com/paddix/p/5405678.html
3. https://blog.csdn.net/javazejian/article/details/72828483
請描述synchronized底層語義以及原理
Java 虛擬機中的同步(Synchronization)基於進入和退出管程(Monitor)對象實現
你是怎么知道monitorenter的?
用javap命令進行反編譯. 比如有這樣一個java源代碼: Main.java
javac Main.java javap -c Main.class
就可以看到反編譯的代碼了.
方法級的synchronized也是根據monitor實現的嗎?
方法級的同步是隱式,即無需通過字節碼指令來控制的,它實現在方法調用和返回操作之中。JVM可以從方法常量池中的方法表結構(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標志區分一個方法是否同步方法。當方法調用時,調用指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設置,如果設置了,執行線程將先持有monitor(虛擬機規范中用的是管程一詞), 然后再執行方法,最后再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執行期間,執行線程持有了monitor,其他任何線程都無法再獲得同一個monitor。如果一個同步方法執行期間拋 出了異常,並且在方法內部無法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放。
描述一下等待喚醒機制與synchronized的聯系?
所謂等待喚醒機制本篇主要指的是notify/notifyAll和wait方法,在使用這3個方法時,必須處於synchronized代碼塊或者synchronized方法中,否則就會拋出IllegalMonitorStateException異常,這是因為調用這幾個方法前必須拿到當前對象的監視器monitor對象,也就是說notify/notifyAll和wait方法依賴於monitor對象,在前面的分析中,我們知道monitor 存在於對象頭的Mark Word 中(存儲monitor引用指針),而synchronized關鍵字可以獲取 monitor ,這也就是為什么notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調用的原因。
wait()和sleep的區別?
與sleep方法不同的是, wait方法調用完成后,線程將被暫停,但wait方法將會釋放當前持有的監視器鎖(monitor),直到有線程調用notify/notifyAll方法后方能繼續執行,而sleep方法只讓線程休眠並不釋放鎖。同時notify/notifyAll方法調用后,並不會馬上釋放監視器鎖,而是在相應的synchronized(){}/synchronized方法執行結束后才自動釋放鎖。
什么是虛假喚醒?
舉個例子,我們現在有一個生產者-消費者隊列和三個線程。
1) 1號線程從隊列中獲取了一個元素,此時隊列變為空。
2) 2號線程也想從隊列中獲取一個元素,但此時隊列為空,2號線程便只能進入阻塞(cond.wait()),等待隊列非空。
3) 這時,3號線程將一個元素入隊,並調用cond.notify()喚醒條件變量。
4) 處於等待狀態的2號線程接收到3號線程的喚醒信號,便准備解除阻塞狀態,執行接下來的任務(獲取隊列中的元素)。
5) 然而可能出現這樣的情況:當2號線程准備獲得隊列的鎖,去獲取隊列中的元素時,此時1號線程剛好執行完之前的元素操作,返回再去請求隊列中的元素,1號線程便獲得隊列的鎖,檢查到隊列非空,就獲取到了3號線程剛剛入隊的元素,然后釋放隊列鎖。
6) 等到2號線程獲得隊列鎖,判斷發現隊列仍為空,1號線程“偷走了”這個元素,所以對於2號線程而言,這次喚醒就是“虛假”的,它需要再次等待隊列非空。
描述一下Monitor中的內部隊列的作用?
ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 后進入 _Owner 區域並把monitor中的owner變量設置為當前線程同時monitor中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復為null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其他線程進入獲取monitor(鎖)。如下圖所示
哪些對象可以作為synchronized鎖?
Java中任意對象可以作為鎖. monitor對象存在於每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是notify/notifyAll/wait等方法存在於頂級對象Object中的原因(關於這點稍后還會進行分析)
一般而言,synchronized使用的鎖對象是存儲在Java對象頭里的,jvm中采用2個字來存儲對象頭(如果對象是數組則會分配3個字,多出來的1個字記錄的是數組長度)
對象頭的結構是什么樣的?
Hotspot中對象在內存中的結構:
從上面的這張圖里面可以看出,對象在內存中的結構主要包含以下幾個部分:
1. Mark Word:對象的Mark Word部分占4個字節,其內容是一系列的標記位,比如輕量級鎖的標記位,偏向鎖標記位等等。
2. Class對象指針:Class對象指針的大小也是4個字節,其指向的位置是對象對應的Class對象(其對應的元數據對象)的內存地址
3. 對象實際數據:這里面包括了對象的所有成員變量,其大小由各個成員變量的大小決定,比如:byte和boolean是1個字節,short和char是2個字節,int和float是4個字節,long和double是8個字節,reference是4個字節
4. 對齊:最后一部分是對齊填充的字節,按8個字節填充。
根據上面的圖,那么我們可以得出Integer的對象的結構如下:
Integer只有一個int類型的成員變量value,所以其對象實際數據部分的大小是4個字節,然后再在后面填充4個字節達到8字節的對齊,所以可以得出Integer對象的大小是16個字節。
因此,我們可以得出Integer對象的大小是原生的int類型的4倍。
關於對象的內存結構,需要注意數組的內存結構和普通對象的內存結構稍微不同,因為數據有一個長度length字段,所以在對象頭后面還多了一個int類型的length字段,占4個字節,接下來才是數組中的數據,如下圖:
對象頭中的MarkWord 結構如下:
鎖狀態 |
25 bit |
4bit |
1bit |
2bit |
||
23bit |
2bit |
是否是偏向鎖 |
鎖標志位 |
|||
輕量級鎖 |
指向棧中鎖記錄的指針 |
00 |
||||
重量級鎖 |
指向互斥量(重量級鎖)的指針 |
10 |
||||
GC標記 |
空 |
11 |
||||
偏向鎖 |
線程ID |
Epoch |
對象分代年齡 |
1 |
01 |
|
無鎖 |
對象的hashCode |
對象分代年齡 |
0 |
01 |
synchronized實現可重入的count存在哪兒?
ObjectMonitor在openjdk中的源碼路徑: openjdk/hotspot/src/share/vm/runtime/objectMonitor.hpp
數據結構里有_count一項, 用於在重入時進行自增操作, 釋放時進行自減操作.
// initialize the monitor, exception the semaphore, all other fields // are simple integers or pointers ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }
Java虛擬機對synchronized的優化
鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨着鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級.
自旋鎖與自適應自旋
偏向鎖
偏向鎖是Java 6之后加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此為了減少同一線程獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個線程獲得了鎖,那么鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程序的性能。所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個線程申請相同的鎖。但是對於鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗后,並不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。下面我們接着了解輕量級鎖。
偏向鎖的獲取
偏向鎖獲取過程:
(1)訪問Mark Word中偏向鎖的標識是否設置成1,鎖標志位是否為01——確認為可偏向狀態。
(2)如果為可偏向狀態,則測試線程ID是否指向當前線程,如果是,進入步驟(5),否則進入步驟(3)。
(3)如果線程ID並未指向當前線程,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中線程ID設置為當前線程ID,然后執行(5);如果競爭失敗,執行(4)。
(4)如果CAS獲取偏向鎖失敗,則表示有競爭。當到達全局安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續往下執行同步代碼。
(5)執行同步代碼。
偏向鎖的撤銷
偏向鎖的釋放:
偏向鎖的撤銷在上述第四步驟中有提到。偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態,撤銷偏向鎖后恢復到未鎖定(標志位為“01”)或輕量級鎖(標志位為“00”)的狀態。
關閉偏向鎖
偏向鎖升級為輕量級鎖
輕量級鎖
倘若偏向鎖失敗,虛擬機並不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之后加入的),此時Mark Word 的結構也變為輕量級鎖的結構。輕量級鎖能夠提升程序性能的依據是“對絕大部分的鎖,在整個同步周期內都不存在競爭”,注意這是經驗數據。需要了解的是,輕量級鎖所適應的場景是線程交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。
輕量級鎖加鎖
輕量級鎖的加鎖過程
(1)在代碼進入同步塊的時候,如果同步對象鎖狀態為無鎖狀態(鎖標志位為“01”狀態,是否為偏向鎖為“0”),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。這時候線程堆棧與對象頭的狀態如圖2.1所示。
(2)拷貝對象頭中的Mark Word復制到鎖記錄中。
(3)拷貝成功后,虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,並將Lock record里的owner指針指向object mark word。如果更新成功,則執行步驟(3),否則執行步驟(4)。
(4)如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標志位設置為“00”,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如圖2.2所示。
(5)如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹為重量級鎖,鎖標志的狀態值變為“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態。 而當前線程便嘗試使用自旋來獲取鎖,自旋就是為了不讓線程阻塞,而采用循環去獲取鎖的過程。
圖2.1 輕量級鎖CAS操作之前堆棧與對象的狀態
圖2.2 輕量級鎖CAS操作之后堆棧與對象的狀態
輕量級鎖解鎖
輕量級鎖的解鎖過程:
(1)通過CAS操作嘗試把線程中復制的Displaced Mark Word對象替換當前的Mark Word。
(2)如果替換成功,整個同步過程就完成了。
(3)如果替換失敗,說明有其他線程嘗試過獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的線程。
鎖消除
消除鎖是虛擬機另外一種鎖的優化,這種優化更徹底,Java虛擬機在JIT編譯時(可以簡單理解為當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間,如下StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬於一個局部變量,並且不會被其他線程所使用,因此StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除。
public class StringBufferRemoveSync { public void add(String str1, String str2) { //StringBuffer是線程安全,由於sb只會在append方法中使用,不可能被其他線程引用 //因此sb屬於不可能共享的資源,JVM會自動消除內部的鎖 StringBuffer sb = new StringBuffer(); sb.append(str1).append(str2); } public static void main(String[] args) { StringBufferRemoveSync rmsync = new StringBufferRemoveSync(); for (int i = 0; i < 10000000; i++) { rmsync.add("abc", "123"); } } }
鎖粗化
鎖粗化的概念應該比較好理解,就是將多次連接在一起的加鎖、解鎖操作合並為一次,將多個連續的鎖擴展成一個范圍更大的鎖。舉個例子:
package com.paddx.test.string; public class StringBufferTest { StringBuffer stringBuffer = new StringBuffer(); public void append(){ stringBuffer.append("a"); stringBuffer.append("b"); stringBuffer.append("c"); } }