- 問題一:通知丟失
- 問題分析
- 問題二:假喚醒
- 等待/通知的典型正規化
- 等待方遵循原則
- 通知方遵循原則
也許我們只知道wait和notify是實現執行緒通訊的,同時要使用synchronized包住,其實在開發中知道這個是遠遠不夠的。接下來看看兩個常見的問題。
問題一:通知丟失
建立2個執行緒,一個執行緒負責計算,一個執行緒負責獲取計算結果。
public class Calculator extends Thread {
int total;
@Override
public void run() {
synchronized (this){
for(int i = 0; i 101; i++){
total += i;
}
this.notify();
}
}
}
public class ReaderResult extends Thread {
Calculator c;
public ReaderResult(Calculator c) {
this.c = c;
}
@Override
public void run() {
synchronized (c) {
try {
System.out.println(Thread.currentThread() + "等待計算結...");
c.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "計算結果為:" + c.total);
}
}
public static void main(String[] args) {
Calculator calculator = new Calculator();
//先啟動獲取計算結果執行緒
new ReaderResult(calculator).start();
calculator.start();
}
}
我們會獲得預期的結果:
Thread[Thread-1,5,main]等待計算結...
Thread[Thread-1,5,main]計算結果為:5050
但是我們修改為先啟動計算執行緒呢?
calculator.start();
new ReaderResult(calculator).start();
這是獲取結算結果執行緒一直等待:
Thread[Thread-1,5,main]等待計算結...
問題分析
打印出執行緒堆疊:
"Thread-1" prio=5 tid=0x00007f983b87e000 nid=0x4d03 in Object.wait() [0x0000000118988000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007d56fb4d0> (a com.concurrent.waitnotify.Calculator)
at java.lang.Object.wait(Object.java:503)
at com.concurrent.waitnotify.ReaderResult.run(ReaderResult.java:18)
- locked <0x00000007d56fb4d0> (a com.concurrent.waitnotify.Calculator)
可以看出ReaderResult在Calculator上等待。發生這個現象就是常說的通知丟失,在獲取通知前,通知提前到達,我們先計算結果,計算完後再通知,但是這個時候獲取結果沒有在等待通知,等到獲取結果的執行緒想獲取結果時,這個通知已經通知過了,所以就發生丟失,那我們該如何避免?可以設定變數表示是否被通知過,修改程式碼如下:
public class Calculator extends Thread {
int total;
boolean isSignalled = false;
@Override
public void run() {
synchronized (this) {
isSignalled = true;//已經通知過
for (int i = 0; i 101; i++) {
total += i;
}
this.notify();
}
}
}
public class ReaderResult extends Thread {
Calculator c;
public ReaderResult(Calculator c) {
this.c = c;
}
@Override
public void run() {
synchronized (c) {
if (!c.isSignalled) {//判斷是否被通知過
try {
System.out.println(Thread.currentThread() + "等待計算結...");
c.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "計算結果為:" + c.total);
}
}
}
public static void main(String[] args) {
Calculator calculator = new Calculator();
new ReaderResult(calculator).start();
calculator.start();
}
}
問題二:假喚醒
兩個執行緒去刪除陣列的元素,當沒有元素的時候等待,另一個執行緒新增一個元素,新增完後通知刪除資料的執行緒。
public class EarlyNotify{
private List list;
public EarlyNotify() {
list = Collections.synchronizedList(new LinkedList());
}
public String removeItem() throws InterruptedException {
synchronized ( list ) {
if ( list.isEmpty() ) { //問題在這
list.wait();
}
//刪除元素
String item = (String) list.remove(0);
return item;
}
}
public void addItem(String item) {
synchronized ( list ) {
//新增元素
list.add(item);
//新增後,通知所有執行緒
list.notifyAll();
}
}
private static void print(String msg) {
String name = Thread.currentThread().getName();
System.out.println(name + ": " + msg);
}
public static void main(String[] args) {
final EarlyNotify en = new EarlyNotify();
Runnable runA = new Runnable() {
public void run() {
try {
String item = en.removeItem();
} catch ( InterruptedException ix ) {
print("interrupted!");
} catch ( Exception x ) {
print("threw an Exception!!!\n" + x);
}
}
};
Runnable runB = new Runnable() {
public void run() {
en.addItem("Hello!");
}
};
try {
//啟動第一個刪除元素的執行緒
Thread threadA1 = new Thread(runA, "threadA1");
threadA1.start();
Thread.sleep(500);
//啟動第二個刪除元素的執行緒
Thread threadA2 = new Thread(runA, "threadA2");
threadA2.start();
Thread.sleep(500);
//啟動增加元素的執行緒
Thread threadB = new Thread(runB, "threadB");
threadB.start();
Thread.sleep(1000); // wait 10 seconds
threadA1.interrupt();
threadA2.interrupt();
} catch ( InterruptedException x ) {}
}
}
結果:
threadA1: threw an Exception!!!
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
這裡發生了假喚醒,當新增完一個元素然後喚醒兩個執行緒去刪除,這個只有一個元素,所以會丟擲陣列越界,這時我們需要喚醒的時候在判斷一次是否還有元素。
修改程式碼:
public String removeItem() throws InterruptedException {
synchronized ( list ) {
while ( list.isEmpty() ) { //問題在這
list.wait();
}
//刪除元素
String item = (String) list.remove(0);
return item;
}
}
等待/通知的典型正規化
從上面的問題我們可歸納出等待/通知的典型正規化。該正規化分為兩部分,分別針對等待方(消費者)和通知方(生產者)。
等待方遵循原則如下:
- 獲取物件的鎖
- 如果條件不滿足,那麼呼叫物件的wait()方法,被通知後仍要檢查條件
- 條件滿足則執行對應的邏輯
對應偽程式碼如下:
synchronized(物件){
while(條件不滿足){
物件.wait();
}
對應的處理邏輯
}
通知方遵循原則如下:
- 獲得物件的鎖
- 改變條件
- 通知所以等待在物件上的執行緒
對應偽程式碼如下:
synchronized(物件){
改變條件
物件.notifyAll();
}