也許我們只知道wait和notify是實現(xiàn)線程通信的吼拥,同時要使用synchronized包住倚聚,其實在開發(fā)中知道這個是遠遠不夠的。接下來看看兩個常見的問題凿可。
問題一:通知丟失
創(chuàng)建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上等待白热。發(fā)生這個現(xiàn)象就是常說的通知丟失,在獲取通知前粗卜,通知提前到達屋确,我們先計算結果,計算完后再通知续扔,但是這個時候獲取結果沒有在等待通知攻臀,等到獲取結果的線程想獲取結果時,這個通知已經(jīng)通知過了纱昧,所以就發(fā)生丟失刨啸,那我們該如何避免?可以設置變量表示是否被通知過,修改代碼如下:
public class Calculator extends Thread {
int total;
boolean isSignalled = false;
@Override
public void run() {
synchronized (this) {
isSignalled = true;//已經(jīng)通知過
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();
}
}
問題二:假喚醒
兩個線程去刪除數(shù)組的元素砌些,當沒有元素的時候等待呜投,另一個線程添加一個元素,添加完后通知刪除數(shù)據(jù)的線程存璃。
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
這里發(fā)生了假喚醒,當添加完一個元素然后喚醒兩個線程去刪除纵东,這個只有一個元素粘招,所以會拋出數(shù)組越界,這時我們需要喚醒的時候在判斷一次是否還有元素偎球。
修改代碼:
public String removeItem() throws InterruptedException {
synchronized ( list ) {
while ( list.isEmpty() ) { //問題在這
list.wait();
}
//刪除元素
String item = (String) list.remove(0);
return item;
}
}
等待/通知的典型范式
從上面的問題我們可歸納出等待/通知的典型范式洒扎。該范式分為兩部分,分別針對等待方(消費者)和通知方(生產(chǎn)者)衰絮。
等待方遵循原則如下:
獲取對象的鎖
如果條件不滿足袍冷,那么調用對象的wait()方法,被通知后仍要檢查條件
條件滿足則執(zhí)行對應的邏輯
對應偽代碼如下:
synchronized(對象){
while(條件不滿足){
對象.wait();
}
對應的處理邏輯
}
通知方遵循原則如下:
獲得對象的鎖
改變條件
通知所以等待在對象上的線程
對應偽代碼如下:
synchronized(對象){
改變條件
對象.notifyAll();
}