[面試]synchronized


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

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

自旋鎖與自適應自旋

通常我們稱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");
    }
}

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM