wait 和 notify 有坑。。
點擊關(guān)注公眾號,Java干貨及時送達
作者:忘凈空
鏈接:https://www.jianshu.com/p/91d95bb5a4bd
也許我們只知道wait和notify是實現(xiàn)線程通信的,同時要使用synchronized包住,其實在開發(fā)中知道這個是遠遠不夠的。
接下來看看兩個常見的問題。
問題一:通知丟失
創(chuàng)建2個線程,一個線程負責計算,一個線程負責獲取計算結(jié)果。
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() + "等待計算結(jié)...");
c.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "計算結(jié)果為:" + c.total);
}
}
public static void main(String[] args) {
Calculator calculator = new Calculator();
//先啟動獲取計算結(jié)果線程
new ReaderResult(calculator).start();
calculator.start();
}
}
Thread[Thread-1,5,main]等待計算結(jié)...
Thread[Thread-1,5,main]計算結(jié)果為:5050
但是我們修改為先啟動計算線程呢?
calculator.start();
new ReaderResult(calculator).start();
這是獲取結(jié)算結(jié)果線程一直等待:
Thread[Thread-1,5,main]等待計算結(jié)...
問題分析
打印出線程堆棧:
"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)象就是常說的通知丟失,在獲取通知前,通知提前到達,我們先計算結(jié)果,計算完后再通知。Spring Boot 學(xué)習筆記,分享給你。
但是這個時候獲取結(jié)果沒有在等待通知,等到獲取結(jié)果的線程想獲取結(jié)果時,這個通知已經(jīng)通知過了,所以就發(fā)生丟失,那我們該如何避免?可以設(shè)置變量表示是否被通知過。
點擊關(guān)注公眾號,Java干貨及時送達
修改代碼如下:
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() + "等待計算結(jié)...");
c.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "計算結(jié)果為:" + 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ù)組越界,這時我們需要喚醒的時候在判斷一次是否還有元素。Java 最新技術(shù)教程和示例源碼參考:https://github.com/javastacks/javastack
修改代碼:
public String removeItem() throws InterruptedException {
synchronized ( list ) {
while ( list.isEmpty() ) { //問題在這
list.wait();
}
//刪除元素
String item = (String) list.remove(0);
return item;
}
}
等待/通知的典型范式
從上面的問題我們可歸納出等待/通知的典型范式。
該范式分為兩部分,分別針對等待方(消費者)和通知方(生產(chǎn)者)。另外,關(guān)注公眾號Java技術(shù)棧,在后臺回復(fù):面試,可以獲取我整理的 Java 系列面試題和答案,非常齊全。
等待方遵循原則如下:
獲取對象的鎖 如果條件不滿足,那么調(diào)用對象的wait()方法,被通知后仍要檢查條件 條件滿足則執(zhí)行對應(yīng)的邏輯
對應(yīng)偽代碼如下:
synchronized(對象){
while(條件不滿足){
對象.wait();
}
對應(yīng)的處理邏輯
}
通知方遵循原則如下:
獲得對象的鎖 改變條件 通知所以等待在對象上的線程
對應(yīng)偽代碼如下:
synchronized(對象){
改變條件
對象.notifyAll();
}






關(guān)注Java技術(shù)??锤喔韶?/strong>


