java線程總結3--synchronized關鍵字,原理以及相關的鎖

在多線程編程中,synchronized關鍵字很是常見,當咱們須要進行「同步」操做時,咱們不少時候須要該該關鍵字對代碼塊或者方法進行鎖定。被synchronized鎖定的代碼塊,只能同時有一條線程訪問該代碼塊。編程

上面是不少人的認識,固然也是我以前對synchronized關鍵字的淺顯認識,其實上面的觀點存在必定的誤差。在參考了不少文章以及本身動手測試過相關代碼後,我以爲有必要記錄下本身對synchronized關鍵字的一些理解,在這個過程,會簡單說說synchronized關鍵字的具體實現原理。api

 

1、synchronized:synchronized是鎖代碼塊仍是鎖對象?多線程

  synchronized具備同步的功能,更準確說是具備互斥的鎖功能,那麼,它究竟是鎖定了相關的代碼塊仍是說鎖定了對象數據?答案是鎖對象。下面就從synchronized修飾方法和修飾具體代碼塊兩方面理解這個結論:synchronized是鎖對象的。併發

  一、synchronized修飾方法框架

    廢話很少說,先簡單看測試代碼:異步

  代碼一jvm

public class SynchronizedTest1 {
    public static void main(String [] argStrings){
        final Test1 test1 = new Test1();
        new Thread(new Runnable() {
            public void run() {
                try {
                    test1.firstMethod();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        
        new Thread(new Runnable() {
            public void run() {
                test1.secondMethod();
                
            }
        }).start();
}

class Test1{
    public synchronized void firstMethod() throws InterruptedException{
        System.out.println("firstMethod");
        Thread.sleep(2000);
    }
    public void secondMethod(){
        System.out.println("secondMethod");
    }
}

  輸出結果: firstMethod     secondMethod  或者   secondMethod  firstMethod    測試

 而後,看代碼二優化

  

public class SynchronizedTest1 {
    public static void main(String [] argStrings){
        final Test1 test1 = new Test1();
        new Thread(new Runnable() {
            public void run() {
                try {
                    test1.firstMethod();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                test1.thirdMethod();
                
            }
        }).start();
    }
}

class Test1{
    public synchronized void firstMethod() throws InterruptedException{
        System.out.println("firstMethod");
        Thread.sleep(2000);
    }
    public synchronized void thirdMethod(){
        System.out.println("thirdMethod");
    }
}

  結果一直是:firstMethod   thirdMethodthis

  因此,咱們能夠得出如下結論(在理解該結論前讀者能夠先假設每一個對象均有一個鎖對象,具體後面講解synchronized關鍵字原理時會講解):

  synchronize修飾方法時(非靜態方法,靜態方法稍後討論),表示某個線程執行到該方法時會鎖定當前對象,其餘線程不能夠調用該對象中含有synchronized關鍵字的方法,由於這些線程的這些方法要執行前提是要得到該對象的鎖。

  就上面的例子具體說:

  在代碼一中,因爲secondMethod方法沒有synchronized關鍵字修飾,該方法執行無需獲取當前對象的鎖,因此secondMethod和firstMethod執行是並行的;

  在代碼二中,firstMethod和thirdMethod方法均有synchronized關鍵字修飾,兩個方法執行前提是能夠獲取當前對象的鎖,因此二者是沒法同時進行的,由於同一個對象中只有一把鎖。

ok,理解上面的例子以後,估計讀者理解下面的代碼就簡單不少了,好,上代碼:

  代碼三:修飾靜態方法時的情形(只有一個方法有static修飾)

public class SynchronizedTest1 {
    public static void main(String [] argStrings){
        final Test2 test2 = new Test2();
        new Thread(new Runnable() {
            public void run() {
                try {
                    test2.firstMethod();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                test2.thirdMethod();
                
            }
        }).start();
    }
}

class Test2{
    public static synchronized void firstMethod() throws InterruptedException{
        System.out.println("firstMethod");
        Thread.sleep(2000);
    }
    public synchronized void thirdMethod(){
        System.out.println("thirdMethod");
    }
}
執行結果是:firstMethod   thirdMethod 或者thirdMethod   firstMethod

    代碼四:(都有static修飾)

public class SynchronizedTest1 {
    public static void main(String [] argStrings){
        final Test2 test2 = new Test2();
        new Thread(new Runnable() {
            public void run() {
                try {
                    test2.firstMethod();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                test2.thirdMethod();
                
            }
        }).start();
    }
}

class Test2{
    public static synchronized void firstMethod() throws InterruptedException{
        System.out.println("firstMethod");
        Thread.sleep(2000);
    }
    public static synchronized void thirdMethod(){
        System.out.println("thirdMethod");
    }
}

    執行結果是:firstMethod   thirdMethod

    從上面結果咱們能夠知道:synchronized修飾靜態方法時,它會鎖定Class實例對象。

    因此代碼三中因爲firstMethod和thirdMethod鎖定對象不一樣,因此他們能夠並行執行;代碼四中兩個方法都鎖定了class對象,因此沒法並行執行。

  好,上面一大坨的文字,總結一句話起來就是這樣:

  synchronized修飾靜態方法時,它表示鎖定class對象;修飾動態方法時,表示鎖定當前對象(this)。

  

  二、synchronized修飾代碼塊

  其實,上面的講解已經很明確指出了一個事實:synchronized關鍵字就是表示當前代碼鎖住了某個對象,其餘須要獲取該鎖(即被synchronized修飾)的代碼塊須要被執行,必須獲取該對象的鎖,即等到synchronized釋放鎖(其實就是改代碼塊執行完),它纔有可能被執行,只是當synchronized修飾代碼塊時,必須顯式地指出它鎖定了哪一個對象而已。上代碼:

  代碼五:

public class SynchronizedTest1 {
    public static void main(String [] argStrings){
        final Test3 test3 = new Test3();
        new Thread(new Runnable() {
            public void run() {
                try {
                    test3.firstMethod();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                test3.thirdMethod();
                
            }
        }).start();
    }
}

class Test3{
    public void firstMethod() throws InterruptedException{
        synchronized (this) {
            System.out.println("firstMethod");
            Thread.sleep(2000);
        }
    }
    public void thirdMethod(){
        synchronized (this) {
            System.out.println("thirdMethod");
        }
    }
}

  上面代碼和代碼二五執行效果任何區別,實現的功能也是同樣的。

  固然,與直接修飾方法相比,synchronized修飾代碼塊時,this能夠換成其餘對象,咱們鎖定對象的代碼塊也能夠粒度更小。

 好,上面一堆廢話其實歸根到底都是說明一個道理:synchronized修飾的代碼,它表示鎖住了某個對象,被該關鍵字修飾的其餘線程(強調其餘線程,稍後講爲何)的方法若是要執行,也必須獲得該鎖,即synchronized代碼塊執行完。

上面強調其餘線程的意思是,若是同一個線程中被synchronized的方法,則無需獲取該鎖,這是合理的,具體看下面例子估計就明白我在說什麼了:

public class SynchronizedTest1 {
    public static void main(String [] argStrings){
        final Test4 test4 = new Test4();
        new Thread(new Runnable() {
            public void run() {
                try {
                    test4.firstMethod();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

class Test4{
    public synchronized void firstMethod() throws InterruptedException{
        System.out.println("firstMethod");
        Thread.sleep(2000);
        secondMethod();
    }
    public synchronized void secondMethod(){
        System.out.println("secondMethod");
    }
}

  結果是:firstMethod  secondMethod

  好吧,我也以爲我在講廢話,確實這是顯而易見的結果:被synchronized修飾的方法若是互相嵌套調用,同一條線程中並不須要等待上一個synchronized塊執行完,畢竟,這樣會陷入死循環。獲取鎖或者說鎖定對象,是針對不一樣線程而言的。

 

2、synchronized原理詳解

  一、首先理解幾個鎖概念:對象鎖(也就重量鎖),輕量鎖,偏向鎖

  A、對象鎖(重量鎖)

  在多線程環境下,大多數(後面會將爲何是大多數修飾)每一個對象都會有一個monitor對象(關於monitor具體能夠查看jdk api文檔),這個對象其實就是上面咱們解釋synchronized關鍵字時對應的鎖。這個對象鎖負責管理全部訪問該對象的線程,具體管理模型圖能夠參考下面的示意圖:

  對於訪問該對象的線程,併發狀況下,沒有搶到鎖(即對象訪問權)的線程,會被monitor丟進list這個隊列進行等候(固然,自旋鎖狀況例外,後面會具體講解什麼卵是自旋鎖)。而所謂的公平鎖與非公平鎖,就是在notify喚醒list中的等候線程時,list中的線程是按照排隊順序得到鎖仍是一塊兒搶該對象的鎖,前者是公平的,先等候的線程先得到鎖;後者則是不公平的,搶不搶到鎖,和線程等待時間無關,與運氣有關。

  B、輕量鎖

  顯然,重量鎖對於每一個對象都要維護一個monitor對象,開銷確定是挺大的,jvm對此進行了一些優化,這即是輕量鎖出現的緣由。

  輕量鎖大概是這樣一種概念:儘管程序是多線程環境,可是若是訪問當前對象的,該對象並不會new一個monitor,而是在對象的頭部用一個標識字段(貌似是兩位的二進制數)表示鎖,這個就是輕量鎖。在線程嘗試訪問該對象時,該線程會將當前線程私有空間中的對應鎖標識字拷貝到對象頭部,若是修改爲功,表示只有一條線程訪問該對象該對象繼續採用輕量鎖;若是發現輕量鎖已經被其餘線程佔有鎖定,jvm會將輕量鎖升級爲重量鎖。

  C、偏向鎖

  偏向鎖是比輕量鎖更輕量的鎖:當jvm發現當前程序是但線程運行時,變會對對象採起偏向鎖,固然,一旦發現有synchronized多線程執行情形,jvm會把偏向鎖上升爲輕量鎖。

 

  二、synchronized如何實現

  瞭解了上面提到的幾個鎖的概念後,理解synchronized實現原理就很是容易了。

  在多線程環境下,對象對應的monitor管理着須要訪問該對象的全部線程,monitor中會有個變量存儲synchronized獲取次數,在一個線程的中的某個synchronized代碼塊獲取monitor以後,該變量數加一,對於synchronized嵌套情形同樣如此:有多少個synchronized,對應的變量值就是多少,對於其餘線程若是想得到monitor,必須等到當前佔有monitor線程的全部synchronized塊均執行完,每執行完一個synchronized塊,標識變量值減一,變爲0時,其餘線程就能夠開始搶鎖了。

  因此,全部的管理工做均由monitor完成,它是synchronized實現的核心。

  三、其餘知識的補充

  A、自旋鎖

    上面提到,monitor在管理每一個阻塞線程的時候,會把它們放進一個隊列裏面,這是針對非自旋鎖,當monitor被設置爲自旋鎖鎖時,線程不會被掛起放進隊列,而是在作循環,直到獲取monitor的訪問權。因此自旋鎖有什麼存在必要呢?其實,線程的掛起和喚醒的調度過程是須要耗費cpu的,當線程持有鎖的時間很是短時,咱們不必將等待線程掛起,這樣會致使線程的頻繁掛起和喚醒,浪費了cpu資源。固然,自旋鎖中線程在作循環時確定也會消耗資源的,因此如何選擇仍是要衡量調度線程花費大仍是線程原地循環花費大。

    另外,針對上面情形,還有一種鎖叫作自適應自旋鎖,它大概解決了上面提到的一些問題;

  B、自適應自旋鎖

    自適應自旋鎖其實就是在自旋鎖的基礎上加了個自旋數的計數:當循環了必定次數後,該線程尚未獲取鎖時,它就被丟進隊列裏掛起。

 

   好的,synchronized的總結就到這裏了,明天會再總結一篇多線程實現異步框架的文章,到時候也歡迎各位訪問哈!

    另外,本票文章不足之處也請各位大佬多多指教,歡迎拆臺!

相關文章
相關標籤/搜索