摘要:舉例證明 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時,說明鎖被釋放了。
結束語
如果您覺得本文對您有幫助,請點一下“推薦”按鈕,您的【推薦】將是我最大的寫作動力!歡迎各位轉載,但是未經作者本人同意,轉載文章之后必須在文章頁面明顯位置給出作者和原文連接;否則,樓蘭胡楊保留追究法律責任的權利。