最近看帖子,發現一道面試題:
啟動兩個執行緒, 一個輸出 1,3,5,7…99, 另一個輸出 2,4,6,8…100 最後 STDOUT 中按序輸出 1,2,3,4,5…100
題目要求用 Java 的 wait + notify 機制來實現,重點考察對於多執行緒可見性的理解。
wait 和 notify 簡介
wait 和 notify 均為 Object 的方法:
- Object.wait() —— 暫停一個執行緒
- Object.notify() —— 喚醒一個執行緒
從以上的定義中,我們可以瞭解到以下事實:
- 想要使用這兩個方法,我們需要先有一個物件 Object。
- 在多個執行緒之間,我們可以通過呼叫同一個物件的
wait()
和notify()
來實現不同的執行緒間的可見。
物件控制權(monitor)
在使用 wait 和 notify 之前,我們需要先了解物件的控制權(monitor)。在 Java 中任何一個時刻,物件的控制權只能被一個執行緒擁有。如何理解控制權呢?請先看下面的簡單程式碼:
public class ThreadTest {
public static void main(String[] args) {
Object object = new Object();
new Thread(new Runnable() {
@Override
public void run() {
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
直接執行,我們將會得到以下異常:
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.xiangyu.demo.ThreadTest$1.run(ThreadTest.java:10)
at java.lang.Thread.run(Thread.java:748)
出錯的程式碼在:object.wait();
。這裡我們需要了解以下事實:
- 無論是執行物件的 wait、notify 還是 notifyAll 方法,必須保證當前執行的執行緒取得了該物件的控制權(monitor)
- 如果在沒有控制權的執行緒裡執行物件的以上三種方法,就會報 java.lang.IllegalMonitorStateException 異常。
- JVM 基於多執行緒,預設情況下不能保證執行時執行緒的時序性
在上面的示例程式碼中,我們 new 了一個 Thread,但是物件 object 的控制權仍在主執行緒裡。所以會報 java.lang.IllegalMonitorStateException 。
我們可以通過同步鎖來獲得物件控制權,例如:synchronized 程式碼塊。對以上的示例程式碼做改造:
public class ThreadTest {
public static void main(String[] args) {
Object object = new Object();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (object){ // 修改處
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
再次執行,程式碼不再報錯。
我們可以得到以下結論:
- 呼叫物件的
wait()
和notify()
方法,需要先取得物件的控制權 - 可以使用
synchronized (object)
來取得對於 object 物件的控制權
解題
瞭解了物件控制權之後,我們就可以正常地使用 notify 和 wait 了,下面給出我的解題方法,供參考。
public class ThreadTest {
private final Object flag = new Object();
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
ThreadA threadA = threadTest.new ThreadA();
threadA.start();
ThreadB threadB = threadTest.new ThreadB();
threadB.start();
}
class ThreadA extends Thread {
@Override
public void run() {
synchronized (flag) {
for (int i = 0; i <= 100; i += 2) {
flag.notify();
System.out.println(i);
try {
flag.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class ThreadB extends Thread {
@Override
public void run() {
synchronized (flag) {
for (int i = 1; i < 100; i += 2) {
flag.notify();
System.out.println(i);
try {
flag.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
發散:notify()
和notifyAll()
這兩個方法均為 native 方法,在JDK 1.8 中的關於notify()
的JavaDoc如下:
Wakes up a single thread that is waiting on this object`s monitor. If any threads are waiting on this object, one of them is chosen to be awakened.
譯為:
喚醒此 object 控制權下的一個處於 wait 狀態的執行緒。若有多個執行緒處於此 object 控制權下的 wait 狀態,只有一個會被喚醒。
也就是說,如果有多個執行緒在 wait 狀態,我們並不知道哪個執行緒會被喚醒。
在JDK 1.8 中的關於notifyAll()
的JavaDoc如下:
Wakes up all threads that are waiting on this object`s monitor.
譯為:
喚醒所有處於此 object 控制權下的 wait 狀態的執行緒。
所以,我們需要根據實際的業務場景來考慮如何使用。