synchronized 鎖是可重入鎖嗎?如何驗證?


摘要:舉例證明 synchronized鎖 是可重入鎖,並描述可重入鎖的實現原理。

綜述

  先給大家一個結論:synchronized鎖 是可重入鎖

  關於什么是可重入鎖,通俗來說,當線程請求一個由其它線程持有的對象鎖時,該線程會阻塞,而當線程請求由自己持有的對象鎖時,如果該鎖是重入鎖,請求就會成功,否則阻塞。或者說,可重入鎖是同一個線程重復請求由自己持有的鎖對象時,可以請求成功而不會發生死鎖。與多線程並發執行的線程安全不同,可重入強調對單個線程執行時重新進入同一個子程序仍然是安全的。可重入鎖又稱遞歸鎖。

驗證可重入

  假設我們現在不知道它是不是一個可重入鎖,那我們就應該想方設法來驗證它是不是可重入鎖?怎么驗證呢?看下面的代碼!

public class SuperSalesman {

    public int ticketNum = 10;

    public synchronized void superSaleTickets()  {
        ticketNum --;
        System.out.println("父類售票后,剩余票數:" + ticketNum
                + " " + Thread.currentThread().getName());
        try {
            Thread.sleep(30);
        } catch (InterruptedException e) {
            System.out.println("error, " + e);
        }
    }

}

  創建子類:

public class ChildSalesman extends SuperSalesman {

    public static void main(String[] args) {
        ChildSalesman child = new ChildSalesman();
        child.childSaleTickets();
    }

    public synchronized void childSaleTickets() {
        while (ticketNum > 0) {
            ticketNum --;
            System.out.println("子類售票后,余票為:" + ticketNum
                    + " " + Thread.currentThread().getName());
            superSaleTickets(); //允許進入,synchronized的可重入性
        }
    }

    @Override
    public synchronized void superSaleTickets() {
        System.out.println("I am working");
        super.superSaleTickets();
    }
}

  現在運行一下上面的父子類繼承代碼,我們看一下結果:

子類售票后,余票為:9 main
I am working
父類售票后,剩余票數:8 main
子類售票后,余票為:7 main
I am working
父類售票后,剩余票數:6 main
子類售票后,余票為:5 main
I am working
父類售票后,剩余票數:4 main
子類售票后,余票為:3 main
I am working
父類售票后,剩余票數:2 main
子類售票后,余票為:1 main
I am working
父類售票后,剩余票數:0 main

Process finished with exit code 0

  現在可以驗證出 synchronized 是可重入鎖了吧!因為這些方法輸出了相同的線程名稱,表明即使遞歸調用synchronized修飾的方法,也沒有發生死鎖,證明其是可重入的。

  下面是多個方法嵌套調用的例子:

public class SyncTest {

    public static void main(String[] args) {
        LockTest lock = new LockTest();
        lock.method1();
    }
}

public class LockTest {
    public synchronized void method1() {
        System.out.println("method1");
        method2();
    }

    public synchronized void method2() {
        System.out.println("method2");
        method3();
    }

    public synchronized void method3() {
        System.out.println("method3");
    }
}

  執行main方法,控制台打印信息如下,說明不會因為之前已經獲取過鎖還沒釋放而發生阻塞。即同一線程可執行多個持有同一把鎖的方法。

/Library/Java/JavaVirtualMachines/jdk-17.0.2.jdk ...
method1
method2
method3

  可以看到調用的三個方法均得到了執行。我們知道synchronized修飾普通方法時,使用的是對象鎖,也就是SuperSalesman對象。三個方法的鎖都是SuperSalesman對象。我們在子類中執行childSaleTickets方法時,獲取了SuperSalesman對象鎖,然后在childSomeString時調用了重寫父類的superSaleTickets方法,該方法的鎖也是SuperSalesman對象鎖,然后在其中調用父類的superSaleTickets方法,該方法的鎖也是SuperSalesman對象鎖。一個鎖多次請求,而且都成功了,所以synchronized是可重入鎖。

  所以在 java 內部,同一線程在調用自己類中其它 synchronized 方法/塊或調用父類的 synchronized 方法/塊都不會阻礙該線程的執行。就是說同一線程對同一個對象鎖是可重入的,而且同一個線程可以獲取同一把鎖多次,也就是可以多次重入。因為java線程是基於“每個線程(per-thread)”,而不是基於“每次調用(per-invocation)”的(java中線程獲得對象鎖的操作是以線程為粒度的,per-invocation 互斥體獲得對象鎖的操作是以每次調用作為粒度的)。

可重入鎖的實現原理

  看到這里,你終於明白了 synchronized 是一個可重入鎖。但是面試官要再問你,可重入鎖的原理是什么?

解釋一

  可重入鎖實現可重入性原理或機制是:每一把鎖關聯一個線程持有者和計數器,當計數器為 0 時表示該鎖沒有被任何線程持有,那么任何線程都可能獲得該鎖而調用相應的方法;當某一線程請求成功后,JVM會記下鎖的持有線程,並且將計數器置為 1;此時其它線程請求該鎖,則必須等待;而該持有鎖的線程如果再次請求這把鎖,就可以再次拿到這把鎖,同時計數器會遞增;當線程退出同步代碼塊時,計數器會遞減,如果計數器為 0,則釋放該鎖。

解釋二

  通過javap -c SynchronizedLock.class 反編譯,來解析synchronized可重入鎖原理:synchronized通過monitor計數器實現,當執行monitorenter命令時:判斷當前monitor計數器值是否為0,如果為0,則說明當前線程可直接獲取當前鎖對象;否則,判斷當前線程是否和獲取鎖對象線程是同一個線程。若是同一個線程,則monitor計數器累加1,當前線程能再次獲取到鎖;若不是同一個線程,則只能等待其它線程釋放鎖資源。當執行完synchronized鎖對象的代碼后,就會執行monitorexit命令,此時monitor計數器就減1,直至monitor計數器為0時,說明鎖被釋放了。

結束語

  如果您覺得本文對您有幫助,請點一下“推薦”按鈕,您的【推薦】將是我最大的寫作動力!歡迎各位轉載,但是未經作者本人同意,轉載文章之后必須在文章頁面明顯位置給出作者和原文連接;否則,樓蘭胡楊保留追究法律責任的權利。

Reference


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM