1. 程式人生 > >7. 等待和通知機制

7. 等待和通知機制

1. 等待和通知機制的實現

wait() 方法

wait() 是 Object 類的方法,它的作用是使當前執行程式碼的執行緒進行等待,該方法將當前執行緒置入“預執行佇列”中,並在 wait() 所在的程式碼行處停止執行,直到接到通知或者被中斷才能繼續執行。執行緒必須獲得該物件的物件鎖,即只能在同步方法或者同步方法塊中呼叫 wait() 方法,在執行 wait() 方法後,當前執行緒釋放所擁有的物件鎖,如果 wait() 沒有持有物件鎖就執行,會丟擲異常

notify() 方法

notify() 是 Object 類的方法,作用是使停止的執行緒繼續執行,也要在同步方法或者同步塊中呼叫。該方法用來通知那些可能等待該物件的物件鎖的其他執行緒,如果有多個執行緒等待,則由執行緒規劃器隨機挑選處一個呈 wait 狀態的執行緒,對其發出通知 notify,但是被通知的執行緒不會馬上執行 wait 後面的程式碼,因為使用 notify 的執行緒不會馬上釋放鎖,所以被通知的執行緒也不會馬上得到鎖。如果呼叫 notify 時沒有持有物件鎖,就會丟擲異常

使用 wait() 和 notify() 方法進行測試

class MyThread1 extends Thread {

    private Object lock;

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

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName()
                    +
" 開始 wait time = " + System.currentTimeMillis()); try { //wait 使執行緒 thread1 停止執行 lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() +
" 結束 wait time = " + System.currentTimeMillis()); } } } class MyThread2 extends Thread { private Object lock; public MyThread2(Object lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " 開始 notify time = " + System.currentTimeMillis()); //執行緒 thread2 使停止的 thread1 繼續執行 lock.notify(); System.out.println(Thread.currentThread().getName() + " 結束 notify time = " + System.currentTimeMillis()); } } } public class Test extends Thread { public static void main(String[] args) throws InterruptedException { Object lock = new Object(); MyThread1 thread1 = new MyThread1(lock); thread1.start(); Thread.sleep(2000); MyThread2 thread2 = new MyThread2(lock); thread2.start(); } }

結果是:

AAA 開始 wait time = 1540528981658
BBB 開始 notify time = 1540528983660
BBB 結束 notify time = 1540528983661
AAA 結束 wait time = 1540528983661

簡單分析一下整個過程,執行緒 AAA 先執行 run() 方法,獲得了 lock 物件鎖,輸出一行之後因為呼叫 wait() 方法,即執行緒 AAA 被停止,進入等待狀態,同時釋放物件鎖 lock,但是執行緒 AAA 依然在 synchronized 同步塊中;由於執行緒 AAA 停止了,此時執行緒 BBB 開始執行 run() 方法,獲取 lock 物件鎖,輸出一行之後,呼叫 notify() 方法將正在等待的執行緒 AAA 喚醒,使其進入就緒狀態,但執行緒 BBB 並不馬上釋放物件鎖,而是繼續執行自己同步方法中的剩餘方法。只有當執行緒 BBB 執行完 syncrhonized 同步塊之後,才釋放物件鎖,此時被喚醒的執行緒 AAA 才可以重新獲得該物件鎖,然後執行 wait() 方法之後的程式碼

整個過程如圖所示,簡單來說,就是執行緒 AAA 被 wait,然後執行緒 BBB 使用 notify 喚醒執行緒 AAA,但是不立即釋放鎖,直到執行緒 BBB 執行完同步塊之後,才釋放鎖,此時執行緒 AAA 得到鎖,開始執行 wait 後的方法

notify()只能喚醒一個執行緒

如果有多個執行緒處於等待狀態,那麼 notify() 方法只能隨機喚醒一個執行緒,其他沒有沒喚醒的執行緒依舊處於等待狀態。但是可以多次呼叫 notity() 方法來隨機喚醒多個執行緒

//Service2 方法
class Service2 {

    public void testMethod(Object lock) {

        synchronized (lock) {
            System.out.println(Thread.currentThread().getName()
                    + " beg1in wait " + System.currentTimeMillis());
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + " end wait " + System.currentTimeMillis());
        }

    }

}

建立三個執行緒 ThreadA6、ThreadB6 和 ThreadC6

class ThreadA6 extends Thread {

    private Object lock;

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

    @Override
    public void run() {
        Service2 service2 = new Service2();
        service2.testMethod(lock);
    }

}

class ThreadB6 extends Thread {

    private Object lock;

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

    @Override
    public void run() {
        Service2 service2 = new Service2();
        service2.testMethod(lock);
    }

}

class ThreadC6 extends Thread {

    private Object lock;

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

    @Override
    public void run() {
        Service2 service2 = new Service2();
        service2.testMethod(lock);
    }
}

隨機喚醒一個正在等待的執行緒的方法

class NotifyOne extends Thread {

    private Object lock;

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

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("NotifyOne");
            lock.notify();
        }
    }
}

多次呼叫可以隨機喚醒多個正在等待的執行緒的方法

class NotifyMulti extends Thread {

    private Object lock;

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

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("NotifyMulti");
            lock.notify();
            lock.notify();
            lock.notify();
            lock.notify();
            lock.notify();
        }
    }
}

測試方法

public class Test2 {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        ThreadA6 threadA6 = new ThreadA6(lock);
        threadA6.start();
        ThreadB6 threadB6 = new ThreadB6(lock);
        threadB6.start();
        ThreadC6 threadC6 = new ThreadC6(lock);
        threadC6.start();

        Thread.sleep(2000);

        NotifyOne notifyOne = new NotifyOne(lock);
        notifyOne.start();
       	/*NotifyMulti notifyMulti = new NotifyMulti(lock);
        notifyMulti.start();*/
    }

}

結果是:

Thread-0 beg1in wait 1540536524678
Thread-1 beg1in wait 1540536524679
Thread-2 beg1in wait 1540536524679
notifyOne 喚醒了一個執行緒 1540536526679
Thread-0 end wait 1540536526679

由於只能喚醒一個執行緒,另外兩個還處於等待狀態的執行緒因為沒有被喚醒,就處於永遠等待的狀態了

如果呼叫後面被註釋的語句,那麼就能喚醒多個執行緒了

Thread-0 beg1in wait 1540536666626
Thread-2 beg1in wait 1540536666626
Thread-1 beg1in wait 1540536666626
NotifyMulti
Thread-0 end wait 1540536668627
Thread-1 end wait 1540536668627
Thread-2 end wait 1540536668627
notifyAll()可以喚醒多個執行緒

由於不知道有多少個執行緒處於等待的狀態,我們也不可能一直不停呼叫 notify() 方法,這樣會很麻煩,因此可以使用 notifyAll() 方法喚醒全部正在等待的方法

2. 當interrupt方法遇到wait方法

當執行緒呈 wait() 狀態時,呼叫執行緒物件的 interrupt() 方法會出現 InterruptedException 異常

class Service5 {

    public void testMethod(Object lock) {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " be1gin wait "
                + System.currentTimeMillis());
            try {
                lock.wait();
                System.out.println(Thread.currentThread().getName() + " end wait "
                        + System.currentTimeMillis());
            } catch (InterruptedException e) {
                System.out.println("發生異常...");
                e.printStackTrace();
            }
        }
    }

    public void testMethod2(Object lock) {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " begin 2");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " end 2");
        }
    }

}

class ThreadB5 extends Thread {

    private Object lock;

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

    @Override
    public void run() {
        Service5 service5 = new Service5();
        service5.testMethod2(lock);
    }
}

public class ThreadA5 extends Thread {

    private Object lock;

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

    @Override
    public void run() {
        Service5 service5 = new Service5();
        service5.testMethod(lock);
    }

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        ThreadA5 threadA5 = new ThreadA5(lock);
        threadA5.start();
        threadA5.interrupt();
        Thread.sleep(2000);

        ThreadB5 threadB5 = new ThreadB5(lock);
        threadB5.start();
    }
}

結果是:

Thread-0 be1gin wait 1540534325308
發生異常...
java.lang.InterruptedException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at edu.just.Service5.testMethod(ThreadA5.java:10)
	at edu.just.ThreadA5.run(ThreadA5.java:60)
Thread-1 begin 2
Thread-1 end 2

可以看到,報錯了,同時還執行了 wait() 方法之後的程式碼,這是因為:在執行同步程式碼塊的過程中,遇到異常而導致執行緒終止,鎖也會被釋放,此時如果還有執行緒持有物件鎖,那麼 wait 後面的程式碼將不會執行,而是直接報錯

3. 通知時間

wait(long)方法

如果 wait() 方法裡面帶有引數,表示在一段時間內,如果沒有其他執行緒對等待的執行緒進行喚醒,那麼等待的執行緒在超過這個時間之後會自動喚醒

  1. 如果在規定時間之內就被喚醒,那麼會先執行其他執行緒的程式碼,然後在執行 wait 之後的程式碼
  2. 如果在規定直接之外被喚醒,那麼就會先執行 wait 之後程式碼,在執行其他執行緒的程式碼
public class MyRunnable {

    private static Object lock = new Object();

    private static Runnable runnable = new Runnable() {
        @Override
        public void run() {
            synchronized (lock) {

                System.out.println(Thread.currentThread().getName()
                        + " wait begin time " + System.currentTimeMillis());
                try {
                    //規定 1s 之後執行緒自動被喚醒
                    lock.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + " wait end time " + System.currentTimeMillis());
            }
        }
    };

    private static Runnable runnable1 = new Runnable() {
        @Override
        public void run() {
            synchronized (lock) {

                System.out.println(Thread.currentThread().getName()
                        + " wait begin time2 " + System.currentTimeMillis());
                try {
                    lock.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + " wait end time " + System.currentTimeMillis());
            }
        }
    };

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(runnable);
        thread.setName("AAA");
        thread.start();
		//在規定時間之內執行緒 AAA 被喚醒		語句1
        Thread.sleep(1100);
		//在規定時間之外執行緒 AAA 被喚醒     語句2
//        Thread.sleep(900);
        Thread thread2 = new Thread(runnable1);
        thread2.setName("BBB");
        thread2.start();
    }

}

先把語句2註釋掉,結果是:

AAA wait begin time 1540538435802
AAA wait end time 1540538436802
BBB wait begin time2 1540538436902
BBB wait end time 1540538436903

看到此時執行緒 AAA 在被執行緒 BBB 手動喚醒之前就自動喚醒,所以直接執行了 wait 後面的方法

在執行語句2,把語句1註釋,結果是:

AAA wait begin time 1540538528885
BBB wait begin time2 1540538529784
BBB wait end time 1540538529784
AAA wait end time 1540538529885

此時執行緒 BBB 線上程 AAA 被自動喚醒前就將執行緒 AAA 喚醒了,此時先執行完執行緒 BBB 的程式碼,在執行執行緒 AAA wait() 方法後面的程式碼

4.參考

《Java多執行緒程式設計核心技術》