BAT美團滴滴java面試大綱(帶答案版)之三:多線程synchronized


繼續面試大綱系列文章。

 

  從這一篇開始,我們進入ava編程中的一個重要領域---多線程!多線程就像武學中對的吸星大法,理解透了用好了可以得道成仙,俯瞰芸芸眾生;而濫用則會遭其反噬。

  在多線程編程中要渡的首個“劫”,則是Synchronized。了解其底層實現,無論是在面試中還是在平時工作中,都大有裨益。我們知其然,知其所以然,才能得心應手少挖坑。

  我們知道,多線程的核心思想是通過增加線程數量來並發的運行,來提高效率,也就是數量決勝論,而不是質量決勝(提高每個線程的處理能力)。多線程編程中面臨的最大挑戰,是如何解決多個線程同時修改一個公用的變量所帶來的變量值不確定性問題。順着這個思路分析,常用辦法,無非就是,要么對變量動手,在一個線程修改時,變量值被鎖定。要么是對修改的操作動手,在該段代碼執行時,對其加鎖,其他線程不可以在同一時刻進入該段代碼執行。

  Synchronized,正是實現了后一種辦法。

 

Synchronized

 

  1. 問:你平時涉及到多線程編程多不多?談談你對Synchronized鎖的理解
  2. 分析:多從實現原理,工作機制來描述
  3. 答:
    1.   在多線程編程中,為了達到線程安全的目的,我們往往通過加鎖的方式來實現。而Synchronized正是java提供給我們的非常重要的鎖之一。它屬於jvm級別加鎖,底層實現是:在編譯過程中,在指令級別加入一些標識來實現的。例如,同步代碼塊,會在同步代碼塊首尾加入monitorenter和monitorexit字節碼指令,這兩個指令都需要一個reference類型的參數,指明要加鎖和解鎖的對象,同步方法則是通過在修飾符上加acc_synchronized標識實現。在執行到這些指令(標識)時,本質都是獲取、占有、釋放monitor鎖對象實現互斥,也就是說,同一時刻,只能有一個線程能成功的獲取到這個鎖對象。我們看一段加了synchronized關鍵字的代碼編譯后的字節碼。編譯前:
      1 public class test {
      2   public test() { 3  } 4 public static void main(String[] args) { 5 synchronized(new Object()){ 6 int i = 0; 7  } 8  } 9 }

      編譯后:

      public class test extends java.lang.Object{
      public test(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."":()V 4: nop 5: return public static void main(java.lang.String[]); Code: 0: new #2; //class Object 3: dup 4: invokespecial #1; //Method java/lang/Object."":()V 7: dup 8: astore_1 9: monitorenter // Enter the monitor associated with object 10: iconst_0 11: istore_2 12: nop 13: aload_1 14: monitorexit // Exit the monitor associated with object 15: goto 23 18: astore_3 19: aload_1 20: monitorexit // Be sure to exit monitor... 21: aload_3 22: athrow 23: nop 24: return Exception table: from to target type 10 15 18 any 18 21 18 any }

      重點關注14行,和20行。

    2.  在使用Synchronized時,用到的方法是wait和notify(或notifyAll),他們的原理是,調用wait方法后,線程讓出cpu資源,釋放鎖,進入waiting狀態,進入等待隊列【第一個隊列】。當有其他線程調用了notify或者notifyAll喚醒時,會將等待隊列里的線程對象,移入阻塞隊列【第二個隊列】,狀態是blocked,等待鎖被釋放后(這個釋放時機,由虛擬機來決定,人為無法干預),開始競爭鎖。
    3. Synchronized無法中斷正在阻塞隊列或者等待隊列的線程。

  4.擴展:Synchronized提供了以下幾種類型的鎖:偏向鎖、輕量級鎖、重量級鎖。在大部分情況下,並不存在多線程競爭,更常見的是一個線程多次獲取同一個鎖。那么很多的消耗,其實是在鎖的獲取與釋放上。Synchronized一直在被優化,可以說Synchronized雖然推出的較早,但是效率並不比后來推出的Lock差。

  1.  偏向鎖:在jdk1.6中引入,目的是消除在無競爭情況下的同步原語(翻譯成人話就是,即使加了synchronized關鍵字,但是在沒有競爭的時候,沒必要去做獲取-持有-釋放鎖對象的操作,提高程序運行性能)。怎么做呢?當鎖對象第一次被線程A獲取時,虛擬機會把對象頭中的標志位設置為01,也就是代表偏向模式。同時把代表這個線程A的ID,通過CAS方式,更新到鎖對象頭的MarkWord中。相同的線程下次再次申請鎖的時候,只需要簡單的比較線程ID即可。以上操作成功,則成功進入到同步代碼塊。如果此時有其他線程B來競爭該鎖,分兩種情況做不同的處理:
    1. 如果線程A已執行完(並不會主動的修改鎖對象的狀態),會直接撤銷鎖對象的狀態為未鎖定,標志位為00;
    2. 如果線程A還在持有該鎖,則鎖升級為輕量級鎖。  
  2. 輕量級鎖:也是JDK1.6中引入的,輕量級,是相對於使用互斥量的重量級鎖來說的。線程發生競爭鎖的時候,不會直接進入阻塞狀態,而是先嘗試做CAS修改操作,進入自旋,這個過程避免了線程狀態切換的開銷,不過要消耗cpu資源。詳細過程是:
    1.   線程嘗試進入同步代碼塊,如果鎖對象未被鎖定,在當前線程對應的棧幀中,建立鎖記錄的空間,用於存儲鎖對象Mark Word的拷貝。
    2. 然后JVM用CAS方式嘗試將鎖對象的MarkWord內容替換為指向前述“鎖記錄”的指針。如果成功,當前線程則持有了鎖,處於輕量級鎖定狀態;如果失敗,會首先檢查當前MarkWord是否已經指向當前線程棧幀的“鎖記錄”,如果是,就說明當前線程已經擁有了這個鎖,直接重入即可。否則就表明是其他線程持有鎖,那么進入自旋(其實就是重試CAS修改操作)。
    3. 釋放鎖時,是使用CAS來講MarkWord和“鎖記錄”里的內容互換。如果成功,成功釋放;如果事變,表明當前鎖存在競爭(被其他線程修改了MarkWord里的數據),此時,鎖會升級為重量級鎖。
  3. 重量級鎖:也就是我們使用的互斥量方式實現的鎖,當存在多線程競爭時,只要沒拿到鎖,就會進入阻塞狀態,主要消耗是在阻塞-喚起-阻塞-喚起的線程狀態切換上。
  4. 上面介紹的三種類型的鎖,是JVM來負責管理使用哪種類型鎖,以及鎖的升級(注意,沒有降級)。
  5. 這里涉及到鎖升級,對象頭MarkWord等內容,如果詳細說,可能又是一大篇文章了,如果大家感興趣,可以另起一篇。
  6. synchronized就先介紹到這里,下一篇預告:ReentrantLock相關
  7. 如果你看的爽,請點擊右下角的“推薦”,是對小端堅持分享原創的最大鼓勵。也可以關注小端的個人公眾號 :   pnxsxb  ,會分享更多的原創技術文章。

歡迎掃描以下二維碼:


免責聲明!

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



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