[Java多線程 -2]:由淺入深看synchronized的底層實現原理

前言

[從零開啓 Java 多線程 - 1 ]:開胃小菜java

前倆篇文章,咱們聊了聊線程/進程的概念,接着簡單串了一下同步的方式方法。今天咱們就單拎出來synchronized,好好捋一捋它的前世此生。面試

正文

小A:我們前幾天鋪墊了這麼多內容,今天是否是要好好的深挖一下原理的內容了?安全

MDove:沒錯,接下來。我會從常見的synchronized加鎖方式入手;引出Java對象在內存的佈局,以及鎖的存放位置;而後看一看鎖在C++中的簡單實現思路;最後我們從字節碼中,看一下JVM若是識別synchronized。內容不是很難,不會涉及到特別多深奧的內容,大部分是平鋪直敘的介紹,很適合閱讀呦~多線程

小A:快點開始吧,我等不及啦。ide

淺聊synchronized的使用

MDove:提及synchronized的底層實現原來,我們先看看synchronized的倆種加鎖方式:佈局

一、某個對象實例內

此做用域內的synchronized鎖 ,能夠防止多個線程同時訪問這個對象的synchronized方法post

而且一個對象有多個synchronized方法,只要一個線程訪問了其中的一個synchronized方法,其它線程不能同時訪問這個對象中任何一個synchronized方法。性能

此外,不一樣對象實例的synchronized方法是不相干預的。也就是說,其它線程能夠同時訪問此類下的另外一個對象實例中的synchronized方法;學習

public synchronized void method(){
  // TODO
}

public void method(){
  synchronized(this) {
   // TODO
  }
}
複製代碼

二、某個類

此做用域下,能夠防止多個線程同時訪問這個類中的synchronized方法。也就是說此種修飾,能夠對此類的全部對象實例起做用。優化

public void method() {
   synchronized(ClassName.class) {
     // todo
   }
}

public static synchronized method(){
	// TODO
}
複製代碼

MDove:注意一點,synchronized關鍵字是不能繼承的,也就是說,基類的方法synchronized fun(){} 在繼承類中並不自動是synchronized fun(){},而是變成了fun(){}。繼承時,須要顯式的指定它的某個方法爲synchronized方法。有機會你能夠本身寫個demo試一下。

常見錯誤

MDove:你來看一看下面這個demo,有沒有什麼問題?

public class ErrorSyncInstance implements Runnable{
    static int i=0;
    public synchronized void add(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            add();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(new ErrorSyncInstance());
        Thread t2=new Thread(new ErrorSyncInstance());
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
複製代碼

小A:沒以爲有問題吶?這不就是第一種加鎖的方式,鎖實例對象麼?

MDove:既然你都知道是鎖實例對象,那你沒看出來問題麼?雖然咱們使用synchronized修飾了add()。可是卻new了兩個不一樣的實例對象,這也就意味着存在着兩個不一樣的實例對象鎖,所以t1和t2都會進入各自的對象鎖,也就是說t1和t2線程使用的是不一樣的鎖,所以線程安全是沒法保證的。

小A:對對對,沒錯。那解決這種問題,是否是須要用第二種加鎖的方式,鎖住這個類?

MDove:沒錯,解決這種困境的的方式是將synchronized做用於靜態的add方法,這樣的話,對象鎖就當前類,由於類對象只有一個,所以不管new多少個實例對象都是安全的:

小A:那是否是這樣改寫就能夠了?

public static synchronized void add(){
	i++;
}
複製代碼

MDove:沒錯就是這樣,很簡單。接下來讓咱們看一些深刻的內容,鎖的實現。

synchronized鎖的底層實現

MDove:咱們都知道,對象被建立在堆中。而且對象在內存中的存儲佈局方式能夠分爲3塊區域:對象頭、實例數據、對齊填充。其中對象頭,即是咱們今天的主角。

關於實例數據、對齊填充的做用,各位小夥伴能夠參考《深刻理解Java虛擬機》。

MDove:對於對象頭來講,主要是包括倆部分信息:

  • 一、自身運行時的數據,好比:鎖狀態標誌、線程持有的鎖...等等。(此部份內容被稱之爲Mark Word)
存儲內容 標誌位 狀態
對象哈希碼、對象分代年齡 01 未鎖定
指向鎖記錄的指針 00 輕量級鎖定
指向重量級鎖的指針 10 重量級鎖定
11 GC標記
偏向線程ID、偏向時間戳、對象分代年齡 01 可偏向

今天咱們只聊:指向重量級鎖的指針

  • 二、另外一部分是類型指針:JVM經過這個指針來肯定這個對象是哪一個類的實例。

MDove:今天咱們主要聊的是對象頭,第一部分中重量級鎖的內容。

MDove:先讓咱們從宏觀的角度看一看synchronized鎖的實現原理。

synchronized鎖的宏觀實現

MDove:synchronized的對象鎖,其指針指向的是一個monitor對象(由C++實現)的起始地址。每一個對象實例都會有一個 monitor。其中monitor能夠與對象一塊兒建立、銷燬;亦或者當線程試圖獲取對象鎖時自動生成。

monitor是由ObjectMonitor實現(ObjectMonitor.hpp文件,C++實現的),對於咱們來講主要關注的是以下代碼:

ObjectMonitor() {
	// 省略部分變量
    _count        = 0; 
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _EntryList    = NULL ;
}
複製代碼

MDove:咱們能夠看到這裏定義了_WaitSet 和 _EntryList倆個隊列,其中_WaitSet 用來保存每一個等待鎖的線程對象。

小A:那_EntryList呢?

MDove:彆着急,讓咱們先看一下_owner,它指向持有ObjectMonitor對象的線程。當多個線程同時訪問一段同步代碼時,會先存放到 _EntryList 集合中,接下來當線程獲取到對象的monitor時,就會把_owner變量設置爲當前線程。同時count變量+1。若是線程調用wait() 方法,就會釋放當前持有的monitor,那麼_owner變量就會被置爲null,同時_count減1,而且該線程進入 WaitSet集合中,等待下一次被喚醒。

MDove:固然,若當前線程順利執行完方法,也將釋放monitor,重走一遍剛纔的內容,也就是_owner變量就會被置爲null,同時_count減1,而且該線程進入 WaitSet集合中,等待下一次被喚醒。

由於這個鎖對象存放在對象自己,也就是爲何Java中任意對象能夠做爲鎖的緣由。

synchronized代碼塊的底層實現

MDove:我們先寫一個簡單的demo,而後看一下它們的字節碼:

private int i = 0;

public void fun() {
    synchronized (this) {
        i++;
    }
}
複製代碼

MDove:根據虛擬機規範要求,在執行monitorenter指令時,首先要嘗試獲取對象鎖,也就是上文咱們提到了monitor對象。若是這個對象沒有被鎖定,或者當前線程已經擁有了這個對象的鎖,那麼就把鎖的計數器(_count)加1。固然與之對應執行monitorexit指令時,鎖的計數器(_count)也會減1。

MDove:若是當前線程獲取鎖失敗,那麼就會被阻塞住,進入_WaitSet 中,等待鎖被釋放爲止。

小A:等等,我看到字節碼中,有倆個monitorexit指令,這是爲何呢?

MDove:是這樣的,編譯器須要確保方法中調用過的每條monitorenter指令都要執行對應的monitorexit 指令。爲了保證在方法異常時,monitorenter和monitorexit指令也能正常配對執行,編譯器會自動產生一個異常處理器,它的目的就是用來執行 異常的monitorexit指令。而字節碼中多出的monitorexit指令,就是異常結束時,被執行用來釋放monitor的。

小A:咱們剛纔看的是同步代碼塊的原理,那麼直接修飾在方法上呢?也是經過這個倆個指令嗎?

MDove:你別說,還真不是:

public synchronized void fun() {
    i++;
}
複製代碼

MDove:能夠看到:字節碼中並無monitorenter指令和monitorexit指令,取得代之的是ACC_SYNCHRONIZED標識,JVM經過ACC_SYNCHRONIZED標識,就能夠知道這是一個須要同步的方法,進而執行上述同步的過程,也就是_count加1,這些過程。

小A:哦,原來是這樣。一個是用了指令,一個是用的標識呀~對了,我據說synchronized的性能特別低是這樣麼?

MDove:這句話不全對,JDK1.5後對synchronized進行了大刀闊斧的優化,這其中涉及到偏向鎖、輕量級鎖、自旋鎖、鎖消除等手段。時候也不早了,這些內容今天就不展開了。有機會咱們下次再學習吧~

劇終

我是一個應屆生,最近和朋友們維護了一個公衆號,內容是咱們在從應屆生過渡到開發這一路所踩過的坑,以及咱們一步步學習的記錄,若是感興趣的朋友能夠關注一下,一同加油~

我的公衆號:IT面試填坑小分隊
相關文章
相關標籤/搜索