1. 程式人生 > >wait()方法的注意點

wait()方法的注意點

一、問題是什麼?

這個問題是我昨天測試wait()方法的時候偶然發現的,即:

一個執行緒在同步塊或者同步方法中使用同步物件呼叫 wait() 方法的時候,會出現另一個執行緒在同步塊或者同步方法中不使用 notify() 方法,被 wait() 的執行緒就能自動被喚醒的現象。當然這個需要分兩種情況,這個下面會具體說到

我花了半個下午加一個晚上的時間,查了很多資料,問了一些人,才勉強搞懂為什麼會出現這種情況,在這裡記錄一下

二、問題回顧

1.

首先這是我昨天遇到的一個例子,我拿執行緒作為物件鎖

class ThreadB2 extends Thread {
	
    /**
     * 執行緒 BBB 持有物件鎖 this,即當前物件 threadB2
     */
@Override public void run() { synchronized (this) { System.out.println(Thread.currentThread().getName() + " beg " + System.currentTimeMillis()); System.out.println(Thread.currentThread().getName() + " end " + System.currentTimeMillis
()); } } } class ThreadA2 extends Thread { private ThreadB2 threadB2; public ThreadA2(ThreadB2 threadB2) { this.threadB2 = threadB2; } /** * 執行緒 AAA 持有物件鎖 threadB2 */ @Override public void run() { synchronized (threadB2) { System.
out.println(Thread.currentThread().getName() + " beg " + System.currentTimeMillis()); try { System.out.println("wait之前:" + threadB2.isAlive()); threadB2.wait(); System.out.println("wait之後:" + threadB2.isAlive()); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " end " + System.currentTimeMillis()); } } } public class Run2 { public static void main(String[] args) { ThreadB2 threadB2 = new ThreadB2(); threadB2.setName("BBB"); ThreadA2 threadA2 = new ThreadA2(threadB2); threadA2.setName("AAA"); threadA2.start(); threadB2.start(); } }

結果則是有兩種情況,出現最多的一種,也是我覺得最不可思議的一種,是以下這樣:

AAA beg 1540790434044
wait之前:true
BBB beg 1540790434044
BBB end 1540790434044
wait 之後false
AAA end 1540790434044

當然,也有小部分情況,是下面這樣的,也是我覺得”應該就是這種結果“的情況

BBB beg 1540790728351
BBB end 1540790731004
AAA beg 1540790738781
wait之前:false

此時控制檯還是執行的狀態,且永遠不會停止

對於第二種情況我是認可的,表明執行緒 BBB 是先執行 run 方法,且先持有物件鎖 threadB2 的,只有等到執行緒 BBB 執行完 run() 方法裡面的程式碼,退出同步塊,才會釋放自身持有的物件鎖 threadB2;然後執行緒 AAA 拿到物件鎖 threadB2,物件鎖 threadB2 執行 wait() 方法,表明持有該物件鎖的執行緒 AAA 釋放該物件鎖,此時因為執行緒 BBB 已經執行完了(執行緒 BBB 的 isAlive 輸出為 false),沒有執行緒呼叫 notifyAll() 方法來喚醒執行緒 AAA,導致執行緒 AAA 就這樣永遠的等待下去

第一種情況我覺得很奇怪,同樣在執行緒 AAA 中使用物件鎖 threadB2 執行 wait 方法,導致持有該物件鎖的執行緒 AAA 釋放物件鎖,執行緒 AAA 進入等待狀態,按理說,應該是不能執行 wait() 後面的程式碼的,但結果告訴我們,執行了,這也是最使我不解的一個地方:既然沒有使用notify()或者notifyAll()方法,那麼被進入等待狀態的執行緒AAA又是被哪個執行緒喚醒的呢?還是自己喚醒自己?

同時還可以發現,第一種情況,在 wait() 方法之前,執行緒 BBB 還是活著的,第二種情況,執行緒 BBB 因為是先執行完的,所有在 wait() 方法之前就是死的了

在查詢了很多篇部落格和在論壇裡詢問了一些人後,得到了下面的解釋:

執行緒 AAA 以執行緒 BBB 的物件作為鎖物件,如果執行緒 BBB 線上程 AAA 進入等待狀態之前就已經死亡,那麼執行緒 AAA 將永遠等待下去;反之,如果執行緒 BBB 線上程 AAA 進入等待狀態之前沒有死亡,那麼執行緒 AAA 線上程 BBB 執行完之後會被自動喚醒。如果要問是執行緒 AAA 是怎麼被喚醒的,那麼我猜測是執行緒 AAA 持有的物件鎖 threadB2 呼叫 threadB2.notifyAll() 方法將執行緒 AAA 喚醒的

其實,這裡的猜測是有根據的,因為該段程式碼和 join() 方法的原始碼很類似,join() 底層使用的是 wait() 方法,我們看一下 join() 底層的實現

public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    //語句1
    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

注意這裡的語句1,是不是和我例子中使用 wait() 方法類似,只是我沒有判斷當前的鎖物件對應的鎖是否是活著的而與,當我把 wait() 方法改成 join() 方法,也能執行,輸出上面的第一種情況,其實 join() 方法的原理就是我上面對第一種情況的分析,具體對原始碼的分析,可以看我之後的文章

2.

為了加以區分,我還想到了另外一種情況。這和第一個例子不太一樣,不同之處在於此時兩個執行緒都是拿同一個 Object 的物件作為物件鎖的,而第一個例子,是一個執行緒拿另一個執行緒的物件物件作為物件鎖,這兩者是不同的,我們來分析一下這個例子

class ThreadA extends Thread {

    private Object lock;

    public ThreadA(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " beg "
                    + System.currentTimeMillis());

            System.out.println(Thread.currentThread().getName() + " end "
                    + System.currentTimeMillis());
        }
    }
}

class ThreadB extends Thread {

    private Object lock;
    private ThreadA threadA;
	
    //這裡的物件鎖是 lock,穿入的 ThreadA 只是想看一下執行緒 AAA 在 wait 之前是否還存活
    public ThreadB(Object lock, ThreadA threadA) {
        this.lock = lock;
        this.threadA = threadA;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " beg "
                    + System.currentTimeMillis());
            try {
                System.out.println("wait之前:" + threadA.isAlive());
                lock.wait();
                System.out.println("wait之後:" + threadA.isAlive());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " beg "
                    + System.currentTimeMillis());
        }
    }
}

public class Run {

    public static void main(String[] args) {
        Object lock = new Object();
        ThreadA threadA = new ThreadA(lock);
        threadA.setName("AAA");
        ThreadB threadB = new ThreadB(lock,threadA);
        threadB.setName("BBB");

        threadA.start();
        threadB.start();
    }

}

最終的結果只有一種:

AAA beg 1540814622564
AAA end 1540814622564
BBB beg 1540814622564
wait之前:true

此時控制檯還是顯示執行狀態,且永遠不會停止

這種結果對應第一個例子的第二個結果,執行緒 AAA 線上程 BBB 進入等待狀態(lock.wait())之前就已經執行完並死亡了(wait 之前輸出 false),當執行緒 BBB 釋放自己的物件鎖,沒有其他執行緒會將執行緒 BBB 喚醒,此時執行緒 BBB 就會永遠的等待下去

三、總結

基本算是搞懂了,還是要感謝這個過程中幫助過我的人,無論是網上的部落格,還是知識星球裡替我解答疑惑的人,感謝你們。其實不懂的這個過程確實很痛苦,尤其是與你之前所學知識相違背的情況下,但是總能解決的,前提是,你必須能和這個問題“耗下去”