本專欄專一分享大型Bat面試知識,後續會持續更新,喜歡的話麻煩點擊一個關注java
面試官:
synchronize關鍵字在虛擬機執行原理是什麼,能談一談什麼是內存可見性,鎖升級嗎
心理分析:
面試官必定是想深刻考你併發的內容,看你究竟有沒有作過併發處理,大多數開發者在開發App時每每會忽略調併發處理 ,這道題會難住絕大多數人。
求職者:
應該存 鎖的執行原理,鎖優化 ,和java對象頭提及
synchronized的底層是使用操做系統的mutex lock實現的。git
synchronized用的鎖是存在Java對象頭裏的。程序員
JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步。代碼塊同步是使用monitorenter和monitorexit指令實現的,monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處。任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態。github
根據虛擬機規範的要求,在執行monitorenter指令時,首先要去嘗試獲取對象的鎖,若是這個對象沒被鎖定,或者當前線程已經擁有了那個對象的鎖,把鎖的計數器加1;相應地,在執行monitorexit指令時會將鎖計數器減1,當計數器被減到0時,鎖就釋放了。若是獲取對象鎖失敗了,那當前線程就要阻塞等待,直到對象鎖被另外一個線程釋放爲止。面試
注意兩點:安全
一、synchronized同步快對同一條線程來講是可重入的,不會出現本身把本身鎖死的問題;多線程
二、同步塊在已進入的線程執行完以前,會阻塞後面其餘線程的進入。併發
監視器鎖(Monitor)本質是依賴於底層的操做系統的Mutex Lock(互斥鎖)來實現的。每一個對象都對應於一個可稱爲" 互斥鎖" 的標記,這個標記用來保證在任一時刻,只能有一個線程訪問該對象。app
互斥鎖:用於保護臨界區,確保同一時間只有一個線程訪問數據。對共享資源的訪問,先對互斥量進行加鎖,若是互斥量已經上鎖,調用線程會阻塞,直到互斥量被解鎖。在完成了對共享資源的訪問後,要對互斥量進行解鎖。編輯器
mutex的工做方式:
因爲Java的線程是映射到操做系統的原生線程之上的,若是要阻塞或喚醒一條線程,都須要操做系統來幫忙完成,這就須要從用戶態轉換到核心態中,所以狀態轉換須要耗費不少的處理器時間。因此synchronized是Java語言中的一個重量級操做。在JDK1.6中,虛擬機進行了一些優化,譬如在通知操做系統阻塞線程以前加入一段自旋等待過程,避免頻繁地切入到核心態中:
synchronized與java.util.concurrent包中的ReentrantLock相比,因爲JDK1.6中加入了針對鎖的優化措施(見後面),使得synchronized與ReentrantLock的性能基本持平。ReentrantLock只是提供了synchronized更豐富的功能,而不必定有更優的性能,因此在synchronized能實現需求的狀況下,優先考慮使用synchronized來進行同步。
在運行期間,Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化,以32位的JDK爲例:
Synchronized是經過對象內部的一個叫作監視器鎖(monitor)來實現的,監視器鎖本質又是依賴於底層的操做系統的Mutex Lock(互斥鎖)來實現的。而操做系統實現線程之間的切換須要從用戶態轉換到核心態,這個成本很是高,狀態之間的轉換須要相對比較長的時間,這就是爲何Synchronized效率低的緣由。所以,這種依賴於操做系統Mutex Lock所實現的鎖咱們稱之爲「重量級鎖」。
Java SE 1.6爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」:鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。鎖能夠升級但不能降級。
HotSpot的做者通過研究發現,大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到。偏向鎖是爲了在只有一個線程執行同步塊時提升性能。
當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,之後該線程在進入和退出同步塊時不須要進行CAS操做來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖。引入偏向鎖是爲了在無多線程競爭的狀況下儘可能減小沒必要要的輕量級鎖執行路徑,由於輕量級鎖的獲取及釋放依賴屢次CAS原子指令,而偏向鎖只須要在置換ThreadID的時候依賴一次CAS原子指令(因爲一旦出現多線程競爭的狀況就必須撤銷偏向鎖,因此偏向鎖的撤銷操做的性能損耗必須小於節省下來的CAS原子指令的性能消耗)。
偏向鎖獲取過程:
偏向鎖的釋放過程:
如上步驟(4)。偏向鎖使用了一種等到競爭出現才釋放偏向鎖的機制:偏向鎖只有遇到其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位爲「01」)或輕量級鎖(標誌位爲「00」)的狀態。
關閉偏向鎖:
偏向鎖在Java 6和Java 7裏是默認啓用的。因爲偏向鎖是爲了在只有一個線程執行同步塊時提升性能,若是你肯定應用程序裏全部的鎖一般狀況下處於競爭狀態,能夠經過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,那麼程序默認會進入輕量級鎖狀態。
輕量級鎖是爲了在線程近乎交替執行同步塊時提升性能。
輕量級鎖的加鎖過程:
輕量級鎖的解鎖過程:
如上輕量級鎖的加鎖過程步驟(5),輕量級鎖所適應的場景是線程近乎交替執行同步塊的狀況,若是存在同一時間訪問同一鎖的狀況,就會致使輕量級鎖膨脹爲重量級鎖。Mark Word的鎖標記位更新爲10,Mark Word指向互斥量(重量級鎖)
Synchronized的重量級鎖是經過對象內部的一個叫作監視器鎖(monitor)來實現的,監視器鎖本質又是依賴於底層的操做系統的Mutex Lock(互斥鎖)來實現的。而操做系統實現線程之間的切換須要從用戶態轉換到核心態,這個成本很是高,狀態之間的轉換須要相對比較長的時間,這就是爲何Synchronized效率低的緣由。
(具體見前面的mutex lock)
偏向所鎖,輕量級鎖都是樂觀鎖,重量級鎖是悲觀鎖。
鎖消除即刪除沒必要要的加鎖操做。虛擬機即時編輯器在運行時,對一些「代碼上要求同步,可是被檢測到不可能存在共享數據競爭」的鎖進行消除。
根據代碼逃逸技術,若是判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那麼能夠認爲這段代碼是線程安全的,沒必要要加鎖。
看下面這段程序:
public class SynchronizedTest { public static void main(String[] args) { SynchronizedTest test = new SynchronizedTest(); for (int i = 0; i < 100000000; i++) { test.append("abc", "def"); } } public void append(String str1, String str2) { StringBuffer sb = new StringBuffer(); sb.append(str1).append(str2); } }
雖然StringBuffer的append是一個同步方法,可是這段程序中的StringBuffer屬於一個局部變量,而且不會從該方法中逃逸出去(即StringBuffer sb的引用沒有傳遞到該方法外,不可能被其餘線程拿到該引用),因此其實這過程是線程安全的,能夠將鎖消除。
若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,甚至加鎖操做是出如今循環體中的,那即便沒有出現線程競爭,頻繁地進行互斥同步操做也會致使沒必要要的性能損耗。
若是虛擬機檢測到有一串零碎的操做都是對同一對象的加鎖,將會把加鎖同步的範圍擴展(粗化)到整個操做序列的外部。
舉個例子:
public class StringBufferTest { StringBuffer stringBuffer = new StringBuffer(); public void append(){ stringBuffer.append("a"); stringBuffer.append("b"); stringBuffer.append("c"); } }
這裏每次調用stringBuffer.append方法都須要加鎖和解鎖,若是虛擬機檢測到有一系列連串的對同一個對象加鎖和解鎖操做,就會將其合併成一次範圍更大的加鎖和解鎖操做,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。
synchronized影響性能的緣由:
二、互斥同步對性能最大的影響是阻塞的實現,由於阻塞涉及到的掛起線程和恢復線程的操做都須要轉入內核態中完成(用戶態與內核態的切換的性能代價是比較大的)
synchronized鎖:對象頭中的Mark Word根據鎖標誌位的不一樣而被複用
更多Android高級面試合集放在github上面了
須要的小夥伴能夠點擊關於我 聯繫我獲取
很是但願和你們一塊兒交流 , 共同進步
也能夠掃一掃, 目前是一名程序員,不只分享 Android開發相關知識,同時還分享技術人成長曆程,包括我的總結,職場經驗,面試經驗等,但願能讓你少走一點彎路。