[面試]synchronized

synchronized

把面試中遇到的問題進行了整理. 本篇文章copy+整理自:html

        1. http://www.cnblogs.com/lingepeiyong/archive/2012/10/30/2745973.htmljava

        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將在異常拋到同步方法以外時自動釋放。app

描述一下等待喚醒機制與synchronized的聯繫?

所謂等待喚醒機制本篇主要指的是notify/notifyAll和wait方法,在使用這3個方法時,必須處於synchronized代碼塊或者synchronized方法中,不然就會拋出IllegalMonitorStateException異常,這是由於調用這幾個方法前必須拿到當前對象的監視器monitor對象,也就是說notify/notifyAll和wait方法依賴於monitor對象,在前面的分析中,咱們知道monitor 存在於對象頭的Mark Word 中(存儲monitor引用指針),而synchronized關鍵字能夠獲取 monitor ,這也就是爲何notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調用的緣由。jvm

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內存結構地址

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的優化

鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨着鎖的競爭,鎖能夠從偏向鎖升級到輕量級鎖,再升級的重量級鎖,可是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級.

自旋鎖與自適應自旋

一般咱們稱Sychronized鎖是一種重量級鎖,是由於在互斥狀態下,沒有獲得鎖的線程會被掛起阻塞,而掛起線程和恢復線程的操做都須要轉入內核態中完成。同時,虛擬機開發團隊也注意到,許多應用上的數據鎖只會持續很短的一段時間,若是爲了這段時間去掛起和恢復線程是不值得的,因此引入了自旋鎖。所謂的自旋,就是讓沒有得到鎖的線程本身運行一段時間的自循環,這就是自旋鎖。自旋鎖能夠經過-XX:+UseSpinning參數來開啓。
但這顯然並非最好的一種方法,不掛起線程的代價就是該線程會一直佔用處理器。若是鎖被佔用的時間很短,自旋等待的效果就會很好,反之,自旋會消耗大量處理器資源。所以,自旋的等待時間必須有必定的限度,若是超過限度尚未得到鎖,就要掛起線程,這個限度默認是10次,可使用-XX:PreBlockSpin改變。
在JDK6之後又引入了自適應自旋鎖,也就說自旋的時間限度不是一個固定值了,而是由上一次同一個鎖的自旋時間及鎖的擁有者狀態來決定。虛擬機認爲,若是同一個鎖對象自旋剛剛成功得到鎖,那麼下一次極可能得到鎖,因此容許此次自旋鎖自旋很長時間、而若是某個鎖不多得到鎖,那麼之後在獲取鎖的過程當中可能忽略到自旋過程。

偏向鎖

偏向鎖是Java 6以後加入的新鎖,它是一種針對加鎖操做的優化手段,通過研究發現,在大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,所以爲了減小同一線程獲取鎖(會涉及到一些CAS操做,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,若是一個線程得到了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變爲偏向鎖結構,當這個線程再次請求鎖時,無需再作任何同步操做,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操做,從而也就提供程序的性能。因此,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續屢次是同一個線程申請相同的鎖。可是對於鎖競爭比較激烈的場合,偏向鎖就失效了,由於這樣場合極有可能每次申請鎖的線程都是不相同的,所以這種場合下不該該使用偏向鎖,不然會得不償失,須要注意的是,偏向鎖失敗後,並不會當即膨脹爲重量級鎖,而是先升級爲輕量級鎖。下面咱們接着瞭解輕量級鎖。

偏向鎖其實是一種鎖優化的,其目的是爲了減小數據在無競爭狀況下的性能消耗。其核心思想就是鎖會偏向第一個獲取它的線程,在接下來的執行過程當中該鎖沒有其餘的線程獲取,則持有偏向鎖的線程永遠不須要再進行同步。

        偏向鎖的獲取

當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏儲存鎖偏向的線程ID。之後該線程在進入和退出同步塊時不須要進行CAS操做來加鎖和解鎖,只須要檢查當前Mark Word中儲存的線程是否指向當前線程,若是成功,表示已經得到對象鎖;若是檢測失敗,則須要再測試一下Mark Word中偏向鎖的標誌是否已經被置爲1(表示當前鎖是偏向鎖):若是沒有則使用CAS操做競爭鎖,若是設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

偏向鎖獲取過程:

  (1)訪問Mark Word中偏向鎖的標識是否設置成1,鎖標誌位是否爲01——確認爲可偏向狀態。

  (2)若是爲可偏向狀態,則測試線程ID是否指向當前線程,若是是,進入步驟(5),不然進入步驟(3)。

  (3)若是線程ID並未指向當前線程,則經過CAS操做競爭鎖。若是競爭成功,則將Mark Word中線程ID設置爲當前線程ID,而後執行(5);若是競爭失敗,執行(4)。

  (4)若是CAS獲取偏向鎖失敗,則表示有競爭。當到達全局安全點(safepoint)時得到偏向鎖的線程被掛起,偏向鎖升級爲輕量級鎖,而後被阻塞在安全點的線程繼續往下執行同步代碼。

  (5)執行同步代碼。

        偏向鎖的撤銷

偏向鎖使用一種等待競爭出現才釋放鎖的機制,因此當有其餘線程嘗試得到鎖時,纔會釋放鎖。偏向鎖的撤銷,須要等到安全點。它首先會暫停擁有偏向鎖的線程,而後檢查持有偏向鎖的線程是否活着,若是不處於活動狀態,則將對象頭設置爲無鎖狀態;若是依然活動,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼從新偏向其餘線程,要麼恢復到無鎖或者標記對象不合適做爲偏向鎖(膨脹爲輕量級鎖),最後喚醒暫停的線程。
 

偏向鎖的釋放:

  偏向鎖的撤銷在上述第四步驟中有提到偏向鎖只有遇到其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位爲「01」)或輕量級鎖(標誌位爲「00」)的狀態。

        關閉偏向鎖

偏向鎖在Java運行環境中默認開啓,可是不會隨着程序啓動當即生效,而是在啓動幾秒種後才激活,可使用參數關閉延遲:
-XX:BiasedLockingStartupDelay=0 
一樣能夠關閉偏向鎖
 -XX:UseBiasedLocking=false,那麼程序默認進入輕量級鎖。

        偏向鎖升級爲輕量級鎖

 

輕量級鎖

假若偏向鎖失敗,虛擬機並不會當即升級爲重量級鎖,它還會嘗試使用一種稱爲輕量級鎖的優化手段(1.6以後加入的),此時Mark Word 的結構也變爲輕量級鎖的結構。輕量級鎖可以提高程序性能的依據是「對絕大部分的鎖,在整個同步週期內都不存在競爭」,注意這是經驗數據。須要瞭解的是,輕量級鎖所適應的場景是線程交替執行同步塊的場合,若是存在同一時間訪問同一鎖的場合,就會致使輕量級鎖膨脹爲重量級鎖。

輕量級鎖是JDK1.6之中加入的新型鎖機制,它並非來代替重量級鎖的,他的本意是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。

        輕量級鎖加鎖

線程在執行同步塊以前,JVM會如今當前線程的棧幀中建立用於儲存鎖記錄的空間(LockRecord),並將對象頭的Mark Word信息複製到鎖記錄中。而後線程嘗試使用CAS將對象頭的MarkWord替換爲指向鎖記錄的指針。若是成功,當前線程得到鎖,而且對象的鎖標誌位轉變爲「00」,若是失敗,表示其餘線程競爭鎖,當前線程便會嘗試自旋獲取鎖。若是有兩條以上的線程競爭同一個鎖,那麼輕量級鎖就再也不有效,要膨脹爲重量級鎖,鎖標誌的狀態變爲「10」,MarkWord中儲存的就是指向重量級鎖(互斥量)的指針,後面等待的線程也要進入阻塞狀態。
 

輕量級鎖的加鎖過程

  (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操做以後堆棧與對象的狀態

 

 

        輕量級鎖解鎖

輕量級鎖解鎖時,一樣經過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");
    }
}
相關文章
相關標籤/搜索