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

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

public 關鍵字 多線程 java 文章

在多線程編程中,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線程總結--synchronized關鍵字,原理以及相關的鎖