Java并发编程实战————可重入内置锁
引言
在《Java Concurrency in Practice》的加鎖機制一節中作者提到:
? ? Java提供一種內置的鎖機制來支持原子性:同步代碼塊。“重入”意味著獲取鎖的操作的粒度是“線程”,而不是調用。當某個線程請求一個由其他線程持有的鎖時,發出請求的線程就會阻塞。然而,由于內置鎖時可重入的,因此如果某個線程試圖獲得一個已經由它自己持有的鎖,那么這個請求就會成功。————《Java Concurrency in Practice》
關于上面引述的這段話,出自書中第21頁,但是從書中給出的例子來看,對于這個概念真的很難理解,有很多問題blowed my mind。而最重要的關鍵,當然是“同一個線程”。
而鎖這個東西,我們在程序中需要通過其他的線程來感知鎖的存在,否則如果我用一個線程執行了一個用synchronized修飾的方法,誰來證明鎖的存在?這也是測試代碼的書寫難點。
測試代碼
public class Test {public static void main(String[] args) throws InterruptedException {// 創建一個子類對象final Child widget = new Child();// 定義線程1Thread th1 = new Thread("th1") {@Overridepublic void run() {System.out.println(super.getName() + ":start...");widget.doSometing();}};// 定義線程2Thread th2 = new Thread("th2") {@Overridepublic void run() {System.out.println(super.getName() + ":start...");/*** 下行在th1剛剛調用子類重寫的父類加鎖方法doSometing()時,* 另一個線程th2直接調用父類的其他加鎖方法會出現等待現象,說明th1調用子類中重寫的加鎖方法會立刻持有父類鎖,* 此時不允許調用父類其他的加鎖方法*/widget.doAnother();/*** 下行在th1開始調用子類重寫的父類加鎖方法后,立刻通過另一個線程th2調用父類的未加鎖方法doNother(),* th2會立刻執行完畢,不需要等待,也就 證明了內置鎖對那些沒有加鎖的方法是不起作用的,也就是說這些沒有加鎖的方法,* 不會因為其他線程持有該類的內置鎖就處于等待或阻塞的狀態而無法執行*/// widget.doNother();/*** 如果說調用doAnother證明了調用重寫的父類加鎖方法會直接持有父類鎖的話,* 那么下行就證明了調用子類的加鎖方法也一定會獲得該類的內置鎖,就算這個方法已經* 持有了父類鎖,也就是說線程th1在執行doSomething()之初就持有了子類鎖和父類鎖兩個鎖,*/// widget.doMyLike();/** th2調用doSometing()是需要等待的,并不是繼承的關系,不是重入,重入是發生在一個線程中的 */// widget.doSometing();}};th1.start();Thread.sleep(100);th2.start();} }class Father {/** 唯一被子類Child重寫的方法 */public synchronized void doSometing() {System.out.println(Thread.currentThread().getName() + ":Father ... do something...");}public synchronized void doAnother() {System.out.println(Thread.currentThread().getName() + ":Father... do another thing...");}public void doNother() {System.out.println(Thread.currentThread().getName() + ":Father... do Nothing...");} }class Child extends Father {@Overridepublic synchronized void doSometing() {try {System.out.println(Thread.currentThread().getName() + ":Child do something...");Thread.sleep(5000);System.out.println(Thread.currentThread().getName() + ":end Child do something...");} catch (InterruptedException e) {e.printStackTrace();}super.doSometing();}public synchronized void doMyLike() {System.out.println(Thread.currentThread().getName() + ":Child do my like...");} }這是補充了網上代碼的測試code,重要的地方都寫了注釋,原版代碼可以參考這篇文章《java內置鎖synchronized的可重入性》
?
這段代碼其實是書中程序清單2-7的代碼擴展和補充,參考文章的例子非常給力,不過還是需要花點頭腦去理解和總結(不過諸位放心,本篇文章絕對不是簡單的拿來主義)。
?
測試code想要表達什么?在main方法中聲明了兩個線程,用th1去制造“可重入”的條件,而th2用于感知鎖的存在。我們說可重入是指同一個線程而言。但是同一個線程如何才可以持有了一個類的鎖后又去調用加鎖代碼塊?
就像上述代碼所示:父類的加鎖方法doSomething()被子類重寫了,不僅重寫了,還又加了個鎖。那么th1在調用子類doSomething()方法的時候,不僅會獲得子類的鎖,還會同時獲得一個父類的鎖,也就是說doSomething()在被th1的調用之初,th1就會立刻獲得子類和父類兩個類的內置鎖。
而一個線程持有了這個類的內置鎖導致的結果是,其他線程需要等待或阻塞執行該類中其他任何加鎖方法,但未加鎖方法不受影響!
因此,根據這個代碼規則,th1此時持有了子類鎖和父類鎖,那么th2在執行父類的doAnother()加鎖方法時就會出現阻塞執行的情況。
但是我突然又有一個想法,為何一個線程同時獲得子類和父類的雙重鎖的條件這么多?如果子類的重寫方法沒有鎖呢?如果父類的方法沒有鎖,子類重寫的方法有鎖呢?這些情況又會是怎樣的執行結果?于是我調整了代碼,將這兩種情況重現測試了一下:
情況一:父類加鎖,子類未加鎖,th1調用子類重寫方法:
父類:
子類:
執行結果:
情況二:父類方法未加鎖,子類重寫后加鎖,th1調用子類重寫的該方法。
父類:
子類:
執行結果:
總結:子類重寫了父類方法時,如果子類該方法有同步鎖,那么不論父類該方法是否加鎖,線程在調用子類的這個方法時都會同時獲得子類和父類雙重鎖,從而影響其他線程調用子類和父類中任何加鎖方法。
(說實話,如果是gif動圖,效果會更明顯一些,因為在子類重寫的doSomething()中,有一個5秒的線程睡眠時間,這樣的測試效果是比較直觀的。)
以上就是關于“重入”的引申理解,即關于線程獲得雙重鎖的知識總結,可能有些繞,而且后面的擴展總結也比較難想到,筆者建議將上面的完整代碼考下來運行一下,體會一下。而且筆者在必要的輸出語句上補充了線程信息,類名信息,便于區分輸出結果和執行順序,線程的睡眠時間也做了調整,便于理解,注釋也盡量做到嚴謹概括。其中最重要的是th2定義中的4種情況,當然可能還有其他的情況我沒有列舉出來,比如子類新增一個同步方法,在th1中調用的時候是否也會獲得父類的內置鎖呢?大家可以去嘗試一下。
總之,再次強調,一旦線程獲得了某個類的內置鎖,其他線程便會阻塞執行該類中的任何同步方法,但該類的非同步方法是不受影響的。
如有疑問,歡迎文末留言!
=======2018.6.21 清晨更新=======================================================
經過檢驗測試,如果子類中新增一個同步方法,例如前面代碼中的synchronized doMyLike(),線程th1調用之后依然獲取了子類和父類雙重鎖。
這樣我們就將前面的概念上升到了更廣泛的高度上:
如果一個線程調用了一個對象的同步方法,那么這個線程不僅持有該類對象的鎖,由于子類對象同時也是父類對象,因此其他線程不能訪問父類中其他的同步方法,使其他線程進入阻塞狀態。
總結
以上是生活随笔為你收集整理的Java并发编程实战————可重入内置锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java8————Optional
- 下一篇: 内部类详解————局部内部类