Java併發——關鍵字synchronized解析

synchronized用法

在Java中,最簡單粗暴的同步手段就是synchronized關鍵字,其同步的三種用法:
①.同步實例方法,鎖是當前實例對象
②.同步類方法,鎖是當前類對象
③.同步代碼塊,鎖是括號裏面的對象

示例:java

public class SynchronizedTest {

    /**
     * 同步實例方法,鎖實例對象
     */
    public synchronized void test() {
    }

    /**
     * 同步類方法,鎖類對象
     */
    public synchronized static void test1() {
    }

    /**
     * 同步代碼塊
     */
    public void test2() {
        // 鎖類對象
        synchronized (SynchronizedTest.class) {
            // 鎖實例對象
            synchronized (this) {

            }
        }
    }
}
複製代碼

synchronized實現

javap -verbose查看上述示例:
編程

從圖中咱們能夠看出:
同步方法:方法級同步沒有經過字節碼指令來控制,它實如今方法調用和返回操做之中。當方法調用時,調用指令會檢查方法ACC_SYNCHRONIZED訪問標誌是否被設置,若設置了則執行線程須要持有管程(Monitor)才能運行方法,當方法完成(不管是否出現異常)時釋放管程。

同步代碼塊:synchronized關鍵字通過編譯後,會在同步塊的先後分別造成monitorenter和monitorexit兩個字節碼指令,每條monitorenter指令都必須執行其對應的monitorexit指令,爲了保證方法異常完成時這兩條指令依然能正確執行,編譯器會自動產生一個異常處理器,其目的就是用來執行monitorexit指令(圖中14-1八、24-30爲異常流程)。

具體看下monitorexit指令作了什麼,在Hotspot源碼中全文搜索monitorenter,在ciTypeFlow.cpp中找到以下:

case Bytecodes::_monitorenter:
    {
      pop_object();
      set_monitor_count(monitor_count() + 1);
      break;
    }
  case Bytecodes::_monitorexit:
    {
      pop_object();
      assert(monitor_count() > 0, "must be a monitor to exit from");
      set_monitor_count(monitor_count() - 1);
      break;
    }
    
    void      pop_object() {
      assert(is_reference(type_at_tos()), "must be reference type");
      pop();
    }
    
    void      pop() {
      debug_only(set_type_at_tos(bottom_type()));
      _stack_size--;
    }
    int         monitor_count() const  { return _monitor_count; }
    void    set_monitor_count(int mc)  { _monitor_count = mc; }
複製代碼

從源碼中咱們發現當線程得到該對象鎖後,計數器就會加一,釋放鎖就會將計數器減一安全

Monitor

每一個對象都擁有本身的監視器,當這個對象由同步塊或者這個對象的同步方法調用時,執行方法的線程必須先獲取該對象的監視器才能進入同步塊和同步方法,若是沒有獲取到監視器的線程將會被阻塞在同步塊和同步方法的入口處,進入到BLOCKED狀態,如圖:數據結構

Monitor是線程私有的數據結構,每一個線程都有一個可用monitor record列表,同時 還有一個全局的可用列表。每個被鎖住的對象都會和一個monitor關聯(對象頭的MarkWord中的LockWord指向monitor的起始地址),同時monitor中有一個owner字段存放擁有該鎖的線程的惟一標識,表示該鎖被這個線程佔用。其結構以下: 多線程

Owner:初始時爲NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖後保存線程惟一標識,當鎖被釋放時又設置爲NULL
EntryQ:關聯一個系統互斥鎖(semaphore),阻塞全部試圖鎖住monitor record失敗的線程
RcThis:表示blocked或waiting在該monitor record上的全部線程的個數
Nest:用來實現重入鎖的計數
HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)
Candidate:用來避免沒必要要的阻塞或等待線程喚醒,由於每一次只有一個線程可以成功擁有鎖,若是每次前一個釋放鎖的線程喚醒全部正在阻塞或等待的線程,會引發沒必要要的上下文切換(從阻塞到就緒而後由於競爭鎖失敗又被阻塞)從而致使性能嚴重降低。Candidate只有兩種可能的值0表示沒有須要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖
併發

鎖優化

jdk1.6中synchronized的實現進行了各類優化,如適應性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖,主要解決三種場景:
①.只有一個線程進入臨界區,偏向鎖
②.多線程未競爭,輕量級鎖
③.多線程競爭,重量級鎖
偏向鎖→輕量級鎖→重量級鎖過程,鎖能夠升級但不能降級,這種策略是爲了提升得到鎖和釋放鎖的效率,源碼解析能夠看佔小狼——synchronized實現性能

偏向鎖

引入偏向鎖的目的是:在沒有多線程競爭的狀況下,儘可能減小沒必要要的輕量級鎖執行路徑。相對於輕量級鎖,偏向鎖只依賴一次CAS原子指令置換ThreadID,不過一旦出現多個線程競爭時必須撤銷偏向鎖,主要校驗是否爲偏向鎖、鎖標識位以及ThreadID。
優化

  • 加鎖
  • ①.獲取對象的對象頭裏的Mark Word
    ②.檢測Mark Word是否爲可偏向狀態,即mark的偏向鎖標誌位爲1,鎖標識位爲01
    ③.若爲可偏向狀態,判斷Mark Word中的線程ID是否爲當前線程ID,若是指向當前線程執行⑥,不然執行④
    ④.經過CAS操做競爭鎖,競爭成功,則將Mark Word的線程ID替換爲當前線程ID,不然執行⑤
    ⑤.經過CAS競爭鎖失敗,證實當前存在多線程競爭,當到達safepoint全局安全點( 這個時間點是上沒有正在執行的代碼),得到偏向鎖的線程被掛起,撤銷偏向鎖,並升級爲輕量級,升級完成後被阻塞在安全點的線程繼續執行同步代碼塊
    ⑥.執行同步代碼塊

  • 解鎖
  • 線程是不會主動去釋放偏向鎖,只有當其它線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,釋放鎖須要等待全局安全點。步驟以下:
    ①.暫停擁有偏向鎖的線程,判斷鎖對象石是否處於被鎖定狀態
    ②.撤銷偏向鎖,恢復到無鎖狀態(01)或者輕量級鎖(00)的狀態

    輕量級鎖

    引入輕量級鎖的主要目的是在多線程沒有競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。若是多個線程在同一時刻進入臨界區,會致使輕量級鎖膨脹升級重量級鎖,因此輕量級鎖的出現並不是是要替代重量級鎖,在有多線程競爭的狀況下,輕量級鎖比重量級鎖更慢this

  • 加鎖
  • ①.獲取對象的對象頭裏的Mark Word
    ②.判斷當前對象是否處於無鎖狀態,即mark的偏向鎖標誌位爲0,鎖標誌位爲 01
    ③.如果,JVM首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced前綴,即Displaced Mark Word),而後執行④;若不是執行⑤
    ④.JVM利用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針,若是成功表示競爭到鎖,則將鎖標誌位變成00(表示此對象處於輕量級鎖狀態),執行同步操做;若是失敗則執行⑤
    ⑤.判斷當前對象的Mark Word是否指向當前線程的棧幀,若是是說明當前線程已經持有這個對象的鎖,則直接執行同步代碼塊;不然說明該鎖對象已經被其餘線程搶佔了,若是有兩條以上的線程爭用同一個鎖,那輕量級鎖就再也不有效,要膨脹爲重量級鎖,鎖標誌位變成10,後面等待的線程將會進入阻塞狀態

  • 解鎖
  • 輕量級鎖的釋放也是經過CAS操做來進行的,主要步驟以下:

    ①.若是對象的Mark Word仍然指向着線程的鎖記錄,執行②
    ②.用CAS操做把對象當前的Mark Word和線程中複製的Displaced Mark Word替換回來,若是成功,則說明釋放鎖成功,不然執行③
    ③.若是CAS操做替換失敗,說明有其餘線程嘗試獲取該鎖,則須要在釋放鎖的同時須要喚醒被掛起的線程
    spa

    重量級鎖

    重量級鎖經過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操做系統的Mutex Lock實現,操做系統實現線程之間的切換須要從用戶態到內核態的切換,切換成本很是高。

    感謝

    芋道源碼——synchronized實現原理 《深刻理解Java虛擬機》 《java併發編程的藝術》

    相關文章
    相關標籤/搜索