JAVA多執行緒wait與notify詳細解析(由生產者和消費者案例引出)
生產者和消費者這個關係是個經典的多執行緒案例。現在我們編寫一個Demo來模擬生產者和消費者之間的關係。
假如有兩個類,一個是資料生產者類DataProvider,另一個是資料消費者類DataConsumer,這兩個類同時對資料類Data進行操作,生產者類負責生產資料,消費者類負責消費資料,下面是對這個過程的描述。
class DataProvider implements Runnable{ private Data data; public DataProvider(Data data) { this.data=data; } public void run() { for(int i=0;i<=50;i++) { if(i%2==0) { data.setName("張三"); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } data.setTag("---學生"); }else { data.setName("李四"); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } data.setTag("---老師"); } } } } class DataConsumer implements Runnable{ private Data data; public DataConsumer(Data data) { this.data=data; } public void run() { for(int i=0;i<50;i++) { try { Thread.sleep(20); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(data.getName()+data.getTag()); } } } class Data{ private String name; private String tag; public void setName(String name) { this.name=name; } public void setTag(String tag) { this.tag=tag; } public String getName() { return this.name; } public String getTag() { return this.tag; } } public class TestDemo{ public static void main(String args[]) { Data data=new Data(); new Thread(new DataProvider(data)).start(); new Thread(new DataConsumer(data)).start(); } }
輸出如下:
張三null
張三null
張三null
張三null
李四---學生
李四---學生
李四---學生
李四---學生
李四---學生
張三---老師
張三---老師
張三---老師
張三---老師
張三---老師
李四---學生
李四---學生
李四---學生
李四---學生
李四---學生
張三---老師
張三---老師
張三---老師
有輸出可以發現問題:張三的值一會是空一回是老師學生,李四的值也發生了變化。這種操作不同步資料有偏差的問題是什麼原因導致的呢?這是因為以前我們經常寫的程式碼都是由主方法呼叫的,但是上面的這個程式碼卻是由多個執行緒對我們的類進行操作,所以問題就產生了。
出現了上述問題我們第一個想到的就是使用synchronized關鍵字來解決。下面對上述程式碼進行修改。
class DataProvider implements Runnable{ private Data data; public DataProvider(Data data) { this.data=data; } public void run() { for(int i=0;i<=50;i++) { if(i%2==0) { data.set("張三","是一個學生"); }else { data.set("李四","是一個老師"); } } } } class DataConsumer implements Runnable{ private Data data; public DataConsumer(Data data) { this.data=data; } public void run() { for(int i=0;i<50;i++) { try { Thread.sleep(500); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } data.get(); } } } class Data{ private String name; private String tag; private boolean flag=true; public synchronized void set(String name,String tag) { this.name=name; this.tag=tag; } public synchronized void get() { try { Thread.sleep(500); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(this.name+"----"+this.tag); } } public class TestDemo{ public static void main(String args[]) { Data data=new Data(); new Thread(new DataProvider(data)).start(); new Thread(new DataConsumer(data)).start(); } }
輸出結果如下:
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是一個學生
張三----是
修改之後資料之間不會亂了,但是這樣會出現一個更加嚴重的問題,那就是資料重複更加嚴重了。synchronized能解決的只是一個同步問題,但是無法解決資料交替問題(註釋兩點內容:以上兩個輸出結果都是隻截取了片段,沒有完整擷取。其次就是根據電腦的執行速度,輸出的結果會有所不同)。
出現上述現象應該怎樣來解決呢?實際上我們可以考慮使用Object類裡面的wait()來讓執行緒進行等待。當生產者執行緒沒有執行完的時候,Data這個類的門牌上亮的是紅燈,當生產者類生產完資料的時候Data這個類才亮綠燈,表示可以取走資料了。
Object類裡面的wait()方法有兩個wait()是死等,意思就說只要不喚醒就一直在哪等著,但是還有一個引數是long型的wait是活等,等夠設定的時間就自動喚醒。既然有等待執行緒,那麼就會有喚醒執行緒,喚醒執行緒主要由兩個:notify和notifyAll兩個,notify只是喚醒第一個等待的執行緒,而notifyAll則是喚醒所有等待的執行緒,至於一次性喚醒這麼多到底誰先執行?誰的優先順序高誰先執行。
下面來看怎麼使用java等待喚醒機制來解決上面出現的問題
class DataProvider implements Runnable{
private Data data;
public DataProvider(Data data) {
this.data=data;
}
public void run() {
for(int i=0;i<=50;i++) {
if(i%2==0) {
this.data.set("張三","是一個學生");
}else {
this.data.set("李四","是一個老師");
}
}
}
}
class DataConsumer implements Runnable{
private Data data;
public DataConsumer(Data data) {
this.data=data;
}
public void run() {
for(int i=0;i<50;i++) {
this.data.get();
}
}
}
class Data{
private String name;
private String tag;
//flag=true表示允許生產但是不允許消費者取走
//flag=false表示允許取走但是不允許生產者生產
private boolean flag=false;
public synchronized void set(String name,String tag) {
if(flag==true) {
try {
super.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
this.name=name;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.tag=tag;
this.flag=true;
super.notify();
}
public synchronized void get() {
if(flag==false) {//正在生產,不能取走
try {
super.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(this.name+"----"+this.tag);
this.flag=false;//能夠取走,此時不能再生產了
super.notify();//喚醒有可能正在等待的其他執行緒
}
}
public class TestDemo{
public static void main(String args[]) {
Data data=new Data();
new Thread(new DataProvider(data)).start();
new Thread(new DataConsumer(data)).start();
}
}
輸出結果:
張三----是一個學生
李四----是一個老師
張三----是一個學生
李四----是一個老師
張三----是一個學生
李四----是一個老師
張三----是一個學生
李四----是一個老師
張三----是一個學生
李四----是一個老師
張三----是一個學生
李四----是一個老師
張三----是一個學生
李四-
如結果,上面出現的問題得到了很好的解決。這就是執行緒休眠和喚醒的詳細解釋。
下面是彩蛋:
sleep()和wait()的區別:
sleep是Thread類裡面定義的方法,到了一定時間可以自動喚醒。
wait是Object類定義的方法,如果要想喚醒必須使用notify方法或者notifyAll方法才能喚醒