1. 程式人生 > >java執行緒總結--synchronized關鍵字,原理以及相關的鎖

java執行緒總結--synchronized關鍵字,原理以及相關的鎖

在多執行緒程式設計中,synchronized關鍵字非常常見,當我們需要進行“同步”操作時,我們很多時候需要該該關鍵字對程式碼塊或者方法進行鎖定。被synchronized鎖定的程式碼塊,只能同時有一條執行緒訪問該程式碼塊。

上面是很多人的認識,當然也是我之前對synchronized關鍵字的淺顯認識,其實上面的觀點存在一定的偏差。在參考了很多文章以及自己動手測試過相關程式碼後,我覺得有必要記錄下自己對synchronized關鍵字的一些理解,在這個過程,會簡單說說synchronized關鍵字的具體實現原理。

 

一、synchronized:synchronized是鎖程式碼塊還是鎖物件?

  synchronized具有同步的功能,更準確說是具有互斥的鎖功能,那麼,它到底是鎖定了相關的程式碼塊還是說鎖定了物件資料?答案是鎖物件。下面就從synchronized修飾方法和修飾具體程式碼塊兩方面理解這個結論:synchronized是鎖物件的。

  1、synchronized修飾方法

    廢話不多說,先簡單看測試程式碼:

  程式碼一

 

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   thirdMethod

  所以,我們可以得出以下結論(在理解該結論前讀者可以先假設每個物件均有一個鎖物件,具體後面講解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)。

  

  2、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塊執行完,畢竟,這樣會陷入死迴圈。獲取鎖或者說鎖定物件,是針對不同執行緒而言的。

 

二、synchronized原理詳解

  1、首先理解幾個鎖概念:物件鎖(也就重量鎖),輕量鎖,偏向鎖

  A、物件鎖(重量鎖)

  在多執行緒環境下,大多數(後面會將為什麼是大多數修飾)每個物件都會有一個monitor物件(關於monitor具體可以檢視jdk api文件),這個物件其實就是上面我們解釋synchronized關鍵字時對應的鎖。這個物件鎖負責管理所有訪問該物件的執行緒,具體管理模型圖可以參考下面的示意圖:

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

  B、輕量鎖

  顯然,重量鎖對於每個物件都要維護一個monitor物件,開銷肯定是挺大的,jvm對此進行了一些優化,這便是輕量鎖出現的原因。

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

  C、偏向鎖

  偏向鎖是比輕量鎖更輕量的鎖:當jvm發現當前程式是但執行緒執行時,變會對物件採取偏向鎖,當然,一旦發現有synchronized多執行緒執行情形,jvm會把偏向鎖上升為輕量鎖。

 

  2、synchronized如何實現

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

  在多執行緒環境下,物件對應的monitor管理著需要訪問該物件的所有執行緒,monitor中會有個變數儲存synchronized獲取次數,在一個執行緒的中的某個synchronized程式碼塊獲取monitor之後,該變數數加一,對於synchronized巢狀情形一樣如此:有多少個synchronized,對應的變數值就是多少,對於其他執行緒如果想獲得monitor,必須等到當前佔有monitor執行緒的所有synchronized塊均執行完,每執行完一個synchronized塊,標識變數值減一,變為0時,其他執行緒就可以開始搶鎖了。

  所以,所有的管理工作均由monitor完成,它是synchronized實現的核心。

  3、其他知識的補充

  A、自旋鎖

    上面提到,monitor在管理每個阻塞執行緒的時候,會把它們放進一個佇列裡面,這是針對非自旋鎖,當monitor被設定為自旋鎖鎖時,執行緒不會被掛起放進佇列,而是在做迴圈,直到獲取monitor的訪問權。所以自旋鎖有什麼存在必要呢?其實,執行緒的掛起和喚醒的排程過程是需要耗費cpu的,當執行緒持有鎖的時間非常短時,我們沒必要將等待執行緒掛起,這樣會導致執行緒的頻繁掛起和喚醒,浪費了cpu資源。當然,自旋鎖中執行緒在做迴圈時肯定也會消耗資源的,所以如何選擇還是要衡量排程執行緒花費大還是執行緒原地迴圈花費大。

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

  B、自適應自旋鎖

    自適應自旋鎖其實就是在自旋鎖的基礎上加了個自旋數的計數:當迴圈了一定次數後,該執行緒還沒有獲取鎖時,它就被丟進佇列裡掛起。

如果想學習Java工程化、高效能及分散式、深入淺出。效能調優、Spring,MyBatis,Netty原始碼分析的朋友可以加我的Java高階架構進階群:180705916,群裡有阿里大牛直播講解技術,以及Java大型網際網路技術的視訊免費分享給大家