開發與維運

萬字長文帶你徹底理解synchronized關鍵字(下)

三、6個常見的使用情況


我們先給出這6種常見的情況,然後一個一個分析。

1、兩個線程同時訪問一個對象的同步方法。

2、兩個線程訪問的是兩個對象的同步方法。

3、兩個線程訪問的是synchronized的靜態方法。

4、兩個線程同時訪問同步方法與非同步方法。

5、一個線程訪問一個類的兩個普通同步方法。

6、同時訪問靜態同步方法和非靜態同步方法。

為了對這6種情況做到心中有數,不至於搞混了,我們畫一張圖,對每一種情況進行分析。

v2-136d77ee721ced3e99598226b2c02365_1440w.jpg

上面是框架圖,下面我們基於開始來分析:

1、兩個線程同時訪問一個對象的同步方法


這種情況對應於以下這張圖:

v2-b05088696c6bb82ada2787c7a6f5815f_1440w.jpg

這種情況很簡單,我們在上面也演示過,結果就是同一個時刻只能有一個方法進入。這裡就不再演示了。

2、兩個線程訪問的是兩個對象的同步方法


這種情況對應於下面這種:

v2-bfc197d02b255129bbb27ef3f1e78448_1440w.jpg

也就是一個方法有兩把鎖,線程1和線程2互不干擾的訪問。鎖是不起作用的。

3、兩個線程訪問的是synchronized的靜態方法


這種情況對應於下面這種情況:

v2-9a2637ef22fab404eeac5fa95fd1541f_1440w.jpg

我們對這種情況來測試一下吧。

public class SynTest6 implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        SynTest6 instance1 = new SynTest6();
        SynTest6 instance2 = new SynTest6();
        Thread thread1 = new Thread(instance1);
        Thread thread2 = new Thread(instance2);
        thread1.start();
        thread2.start();
    }
    @Override
    public void run() {
        method1();
    }
    public synchronized static void method1() {
        try {
            System.out.println(Thread.currentThread().getName() + "進入到了靜態方法");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "離開靜態方法,並釋放鎖");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在這個例子中我們實例化了兩個對象instance1和instance2,並且存放在了兩個不同的線程中,我們測試一下訪問同一個static同步方法你會發現。即使是實例不同,鎖也會生效,也就是同一時刻只能有一個線程進去。

4、兩個線程同時訪問同步方法與非同步方法


這種情況對應於下面這張圖:

v2-16681136dc67f11e9d412f71134bcf3f_1440w.jpg

我們對這種情況使用代碼進行演示一遍:

public class SynTest7 implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        SynTest7 instance1 = new SynTest7();
        Thread thread1 = new Thread(instance1);
        Thread thread2 = new Thread(instance1);
        thread1.start();
        thread2.start();
    }
    @Override
    public void run() {
        
        method1();
        method2();
    }
    public synchronized  void method1() {
        try {
            System.out.println(Thread.currentThread().getName() + "進入到了同步方法");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "離開同步方法");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public  void method2() {
        System.out.println(Thread.currentThread().getName() + "進入了普通方法");
        System.out.println(Thread.currentThread().getName() + "離開了普通方法");
    }
}

在上面的代碼中,我們定義一個對象,但是使用了兩個線程去分別同時訪問同步和非同步方法。我們看結果:

v2-337119eb532f07c0c54b28c0cfefbe00_1440w.jpg

也就是說,同步方法依然會同步執行,非同步方法不會受到任何影響。

5、一個線程訪問一個類的兩個普通同步方法


這種情況對應於下面這張圖:v2-3b2b498a360803a02ae47a4fc5b20d65_1440w.jpg

我們代碼來測試一下:

public class SynTest8 implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        SynTest8 instance1 = new SynTest8();
        Thread thread1 = new Thread(instance1);
        thread1.start();
    }
    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("Thread-0")) {
            method1();
        }else {
            method2();
        }
    }
    public synchronized  void method1() {
        try {
            System.out.println(Thread.currentThread().getName() + "進入到了同步方法1");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "離開同步方法1");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized  void method2() {
        try {
            System.out.println(Thread.currentThread().getName() + "進入到了同步方法2");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "離開同步方法2");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上面這個例子我們創建了一個對象instance1,然後使用一個線程分別去訪問同步方法1和同步方法2。結果呢可想而知,所一定會失效。因為在一開始我們已經驗證了,此時同步方法1和同步方法2中synchronized鎖的就是this對象,所以是同一把鎖。當然會生效。

6、同時訪問靜態同步方法和非靜態同步方法


這種情況對應於下面這張圖:

v2-f75cab667fd3810c3bedd19ece06c4cf_1440w.jpg

我們使用代碼來測試一波:

public class SynTest9 implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        SynTest9 instance1 = new SynTest9();
        Thread thread1 = new Thread(instance1);
        Thread thread2 = new Thread(instance1);
        thread1.start();thread2.start();
    }
    @Override
    public void run() {
            method1();
            method2();
    }
    public synchronized  void method1() {
        try {
            System.out.println(Thread.currentThread().getName() + "進入到了同步方法1");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "離開同步方法1");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized static void method2() {
        try {
            System.out.println(Thread.currentThread().getName() + "進入到了靜態同步方法2");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "離開靜態同步方法2");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在上面的代碼中,我們創建了一個instance實例,使用兩個線程同時訪問普通同步方法和靜態同步方法。下面運行一下,看看輸出結果:

v2-6363d9fd17ae2671197a118e698eb630_1440w.jpg

上面輸出結果表明普通同步方法和靜態同步方法是沒有關聯的,這是為什麼呢?這是因為普通同步方法的鎖是對象,但是靜態同步方法的鎖是類,所以這是兩把鎖。鎖自然也就是失效了。

四、性質


讀到這裡,不知道你是不是已經很疲憊了,反正我寫的是很難受,不過剩下的這些部分才是精華,也是面試或者是工作中提升你zhuangbility的一個點。希望你一定要注意。認真讀下去。

對於synchronized關鍵字主要有兩個性質:可重入性質和不可中斷性質。我們分別來看。

1、可重入性質


什麼是可重入呢?指的是同一線程的外層函數獲得鎖之後,內層函數可以直接再次獲取該鎖。我們舉一個例子來說明,一句話吃著碗裡的看著鍋裡的。嘴裡面還沒吃完就繼續再去拿吃的。這就是可重入。不可重入的意思正好相反,你吃完了這碗飯才能盛下一碗。

可重入的程度可以細分為三種情況,我們分別測試一下:

(1)同一個方法中是不是可重入的。就好比是遞歸調用同步方法。

(2)不同的方法是不是可重入的。就好比是一個同步方法調用另外一個同步方法。

(3)不同的類方法是不是可重入的。

下面我們就是用代碼來測試一遍:

(1)同一個方法是不是可重入的

public class SynTest10 {
    private int a=1;
    public static void main(String[] args) throws InterruptedException {
        SynTest10 instance1 = new SynTest10();
        instance1.method1();
    }   
    public synchronized  void method1() {
        System.out.println("method1: a= " + a);
        if(a == 3) {
            return ;
        }else {
            a++;
            method1();
        }
    }
}

代碼很簡單,也就是我們定義了一個變量a,只要a不等於3,就一直遞歸調用方法method1。我們可以看一下運行結果。

v2-15eb642445d5c8c70a7d97765393434a_1440w.png

也就是說在同一個方法中是可重入的。下面我們接著測試。

(2)不同的方法是不是可重入的


public class SynTest10 {
    public static void main(String[] args) throws InterruptedException {
        SynTest10 instance1 = new SynTest10();
        instance1.method1();
    }   
    public synchronized  void method1() {
        System.out.println("method1");
        method2();
    }
    public synchronized  void method2() {
        System.out.println("method2" );
    }
}

我們在同步方法1中調用了同步方法2。我們同樣測試一下。

v2-558d22ba8d47d3e00567637c3ebeb9c4_1440w.jpg

method1和method2可以依次輸出,說明了在不同的方法中也是可重入的。

(3)、不同的類方法是不是可重入的


既然是不同的類,那麼我們就在這裡定義兩個類,一個是Father,一個是Son。我們讓son調用father中的方法。

public class Father{
    public synchronized void father() {
        System.out.println("父親");
    }
}
class Son extends Father{
    public static void main(String[] args) {
        Son instance1 = new Son();
        instance1.son();
    }   
    public synchronized  void son() {
        System.out.println("兒子");
        super.father();
    }
}

在這裡son類中使用super.father()調用了父類中的synchronized方法,我們測試一下看看輸出結果:

v2-49b613dbe6a1a81b10b228c974ff52f6_1440w.jpg

2、不可中斷性質

不可中斷的意思你可以這樣理解,別人正在打遊戲,你也想玩,你必須要等別人不想玩了你才能去。在java中表示一旦這個鎖被別人搶走了,你必須等待。等別的線程釋放了鎖,你才可以拿到。否則就一直等下去。

這一點看起來是個有點但其實在某些場景下弊端超級大,因為假如拿到鎖得線程永遠的不釋放,那你就要永遠的等下去。

五、底層原理


對於原理,最好的方式就是深入到JVM中去。我們可以編譯看看其字節碼文件,再來分析,因此在這裡舉一個最簡單的例子。

1、定義一個簡單例子


public class SynTest11 {
    private Object object = new Object();
    public void test() {
        synchronized(object){
            System.out.println("java的架構師技術棧");
        }
    }
    
}


2、分析


分析的步驟很簡單,我們通過反編譯字節碼文件。記住我們的類名是SynTest11。

先編譯生成字節碼文件。

v2-3bef59f08a36b065fa3a21d3b274c6e5_1440w.jpg

然後,我們再反編譯字節碼文件。

v2-ff7032c9d4c6d023021118a68cf22205_1440w.jpg

以上我們知道其是就是設置了一個監控器monitor。線程進來那就是monitorenter,線程離開是monitorexit。這就是synchronized關鍵字最基本的原理。

3、可重入原理


在上面我們曾提到可重入的性質,那麼synchronized關鍵字是如何保證的呢?其是工作是由我們的jvm來完成的,線程第一次給對象加鎖的時候,計數為1,以後這個線程再次獲取鎖的時候,計數會依次增加。同理,任務離開的時候,相應的計數器也會減少。

4、從java內存模型分析


java內存模型不是真正存在的,但是我們可以給出一個內存模型。synchronized關鍵字,會對同步的代碼會先寫到工作內存,等synchronized修飾的代碼塊一結束,就會寫入到主內存,這樣保證了同步。

v2-f70a54bbe8b655383c163e7e9727d07f_1440w.jpg

六、缺陷


synchronized關鍵字既有優點也有缺點,而且缺點賊多,所以後來出現了比他更好的鎖。下面我們就來分析一下,這也是面試常問問題。

1、效率低


我們之前曾經分析過synchronized關鍵字是不可中斷的,這也就意味著一個等待的線程如果不能獲取到鎖將會一直等待,而不能再去做其他的事了。

這裡也說明了對synchronized關鍵字的一個改進措施,那就是設置超時時間,如果一個線程長時間拿不到鎖,就可以去做其他事情了。


2、不夠靈活


加鎖和解鎖的時候,每個鎖只能有一個對象處理,這對於目前分佈式等思想格格不入。


3、無法知道是否成功獲取到鎖


也就是我們的鎖如果獲取到了,我們無法得知。既然無法得知我們也就很不容易進行改進。

既然synchronized有這麼多缺陷。所以才出現了各種各樣的鎖。


七、總結


終於寫完了,synchronized涉及到的知識點,以及能夠引出來的知識點超級多,不過只有理解synchronized關鍵字,我們才可以更加深入的學習。本篇文章不可能面面俱到,只能說列出來一些常見的知識點。更加深入的理解我也會在後續的文章中指出。感謝大家的支持。

Leave a Reply

Your email address will not be published. Required fields are marked *