1. 程式人生 > >(實驗)Java一個執行緒用synchronized巢狀鎖多個物件時呼叫wait()只釋放wait函式關聯的所物件還是釋放所有鎖物件

(實驗)Java一個執行緒用synchronized巢狀鎖多個物件時呼叫wait()只釋放wait函式關聯的所物件還是釋放所有鎖物件

實驗是在JDK1.8下做的。

題目起的比較拗口,其實用程式碼說明起來更簡單,如下所示:

public class MultiSynchronizedTest {

    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    private static class Task1 implements Runnable {
        @Override
        public void run() {
            synchronized (lock1) {
                synchronized (lock2) {
                    try {
                        lock1.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

當一個執行緒執行Task1時,通過synchronized順序獲得了lock1和lock2的鎖,然後在最裡層呼叫鎖(lock1或者lock2,下面以lock1為例)的wait()函式,然後按照教科書式的說法,執行緒進入waiting狀態,釋放鎖,等別的執行緒呼叫notify()或者notifyAll()來再次喚醒到runnable狀態。那麼問題來了,釋放鎖是一個籠統的說法,到底是隻釋放wait()函式關聯的物件鎖(即lock1)還是釋放執行緒當時持有的所有鎖(即lock1和lock2)。

直觀上來講,我只呼叫了lock1.wait()函式,當然只釋放lock1。而且我呼叫哪個物件的wait()就只釋放哪個物件的鎖,這樣程式也更可控。在這裡提前先告訴大家,經過實驗,結果確實是上面講的那樣:一個執行緒通過synchronized巢狀鎖住多個物件,然後在最裡層呼叫wait()函式,只釋放wait()函式關聯的鎖物件,而不是釋放執行緒當時持有的全部鎖

但是我們也可以直觀說執行緒呼叫鎖物件的wait()函式時,就是釋放執行緒當時持有的所有鎖嘛——要不通過wait()自身某種回撥機制來釋放,或者JVM使得執行緒進入waiting(或者time_waiting)狀態時會統一把持有的鎖都釋放了。但是直觀歸直觀,資訊科學技術(拔的太高了?IT碼活)永遠是個實踐出真知的領域,Object.wait()是個native函式,看明白原理要看cpp原始碼,JVM就更不用說了,在不研究原始碼的前提下,做個實驗室最方便了。

下面我們用兩把全域性鎖,兩個執行緒和jstack、jps工具來驗證下。完整的程式碼如下:

package com.jxshen.example.jdk.lock;

public class MultiSynchronizedTest {

    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void main(String[] args) {
        Runnable task1 = new Task1();
        Thread thread1 = new Thread(task1, "task1");
        thread1.start();
        Runnable task2 = new Task2();
        Thread thread2 = new Thread(task2, "task2");
        thread2.start();
    }

    private static class Task1 implements Runnable {

        @Override
        public void run() {
            synchronized (lock1) {
                System.out.println("Task1 obtain lock1");
                synchronized (lock2) {
                    System.out.println("Task1 obtain lock2");
                    try {
                        lock1.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    private static class Task2 implements Runnable {

        @Override
        public void run() {
            try {
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2) {
                System.out.println("Task2 obtain lock2");
            }
        }
    }
}

lock1和lock2是兩個靜態全域性鎖,執行緒task1(程式碼裡我把執行緒名和任務名取做一致,到時候看jstack方便點)順序獲得lock1和lock2,並在最裡層馬上呼叫lock1.wait()。為了保證執行緒task2在task1釋放鎖之後嘗試獲取鎖,task2在一開始先sleep 2秒。注意執行緒task2嘗試synchronized的鎖一定要和task1呼叫wait()關聯的鎖不一樣,否則task2馬上能夠獲得鎖。這裡task2嘗試獲取lock2,如果執行緒呼叫wait()只釋放關聯的鎖物件,那麼task2獲取不到lock2,會阻塞在那;否則task2獲取lock2成功,馬上打印出字串。

執行上面的程式,從控制檯(圖1)可以看出執行緒task2並沒有進入synchronized塊。然後我們通過jstack工具看下這兩個執行緒的具體狀態。


圖1

首先在命令列輸入“jps -l”,獲得圖2所示:


圖2

可以找到我們執行程式對應的程序號pid為27584。jps這個jdk工具可以檢視當前使用者java程序的簡要狀態,引數有l和v,具體用法可以網上查詢。注意jps只能給出歸屬當前使用者的java程序,要是想查詢全部的java程序,windows下要利用下工作管理員,linux下需要top或者ps命令。

然後我們在命令列輸入“jstack 27584”,獲得圖3所示:


圖3

jstack具體展示了某個jvm程序在某時刻的堆疊快照。我們可以看到執行緒task1依次獲取了兩個鎖,分別是0x00000000d5a5b500(lock1)和0x00000000d5a5b510(lock2)。隨後執行緒task1進入了waiting(on object monitor)狀態,等待的鎖物件是0x00000000d5a5b500(lock1),對應程式碼裡的lock1.wait()。

然後看執行緒task2,處於blocked(on object monitor)狀態,等待的鎖物件是0x00000000d5a5b510(lock2)。可以證明執行緒task1在wait後並沒有釋放掉所有的鎖,只是釋放了程式碼裡呼叫wait()的鎖。

其實這篇文章的主題是個很瑣碎的細節,實際中遇到的情況很少,而且直觀上也能想到答案。只是現在一些教材和網路文章中,很多對比sleep和wait,只是說“sleep不釋放執行緒持有的鎖,wait釋放執行緒持有的所鎖”,wait釋放鎖、wait釋放鎖、wait釋放鎖...(面試要應付的知識點重複三遍?)。很多初學者僅僅記住這個答案,會導致一些誤解,其實準確的來說是obj.wait()函式只釋放執行緒持有的obj鎖,而不是釋放執行緒所有的鎖,或者說wait()函式之所以是成員函式而不是靜態函式,就是隻和具體的實體關聯(大家可以換位思考下為什麼Thread.sleep()函式是靜態的)。