java 多執行緒之通訊
共享堆記憶體變數屬於通訊的一種,今天要介紹的是使用Object類中的wait和notify方法進行通訊,可以讓兩個執行緒共同地輪流做一件事
再看Object類:Object類是所有類的根父類,現在主要介紹wait方法和notify方法
void wait()讓當前執行緒進入等待狀態,直到別的執行緒將其喚醒(notify或者notifyAll方法)。
void notify()隨機喚醒單個等待狀態的執行緒,被喚醒的執行緒接著執行之前沒完成的操作
void notifyAll()將所有等待狀態的執行緒喚醒
我們接著做沒做完的練習,讓兩個執行緒輪流列印0-100
package chen_chapter_9; public class WaitNotifyTest01 { public static void main(String[] args) { Print p = new Print(); Thread t1 = new Thread(p, "執行緒t1"); Thread t2 = new Thread(p, "執行緒t2"); t1.start(); t2.start(); } } class Print implements Runnable { static int i = 0; private boolean flag = true; public void run() { while (i < 100) { synchronized (this) { //if (i >= 100) {// 退出迴圈的判斷條件 // break; //} if (!flag) { // flag為假就讓執行緒等待 try { this.flag = !flag;//將flag置反 this.wait(); //這個執行緒進入等待狀態,下次才能讓另一個執行緒進入等待 } catch (InterruptedException e) { e.printStackTrace(); } } else { System.out.println(Thread.currentThread().getName() + " : " + (i++)); this.flag = !flag; //將flag置反,下次才能讓另一個執行緒進入等待 this.notify(); //喚醒在等待的執行緒 } } } } } out: 如果沒有else關鍵字,那麼執行緒從等待中被喚醒後會繼續執行進入等待前的操作,這樣會多打印出一個數字100 分析:啟動執行緒t1,t2,可能t1先進入同步程式碼塊,鎖住this物件,t2這時可能進入了run方法,當t1執行完列印 後設置flag=false,呼叫notify方法,但此時沒有等待的執行緒,這句話不起作用,因為在while語句中,t1還滿足 條件,沒有退出迴圈體,也沒有釋放鎖,再執行一次while迴圈體的語句,這時進入到if語句塊,設定flag=true, 被設定為wait,釋放鎖,t2開始執行,列印語句,將flag=false,呼叫notify喚醒t1,但t2沒釋放鎖,接著執行一 次while迴圈體,進入到if語句塊,設定flag=true,被設定成等待,釋放鎖,此時又該t1執行了,列印語句,設定 flag=false,如此迴圈,如果列印語句不在else語句塊中,當執行緒被喚醒後,會往下執行列印語句,導致結果不正確, 加入else語句塊中後,保證每次while迴圈體內只有一個執行緒能執行
三個及多個執行緒之間的通訊,notify方法只能喚醒一個正在等待的執行緒,如果多個執行緒在等待,再用這個方法,最後可能會出現所有執行緒都在等待的問題
notifyAll方法將所有在等待的執行緒喚醒,
注意:
1 synchronized同步程式碼塊鎖住的是哪個物件,就呼叫哪個鎖物件的wait方法和notify,notifyAll方法,在這裡鎖住的是this,所以呼叫this.wait(),this.notify()
2 while和if
if wait()方法在if條件判斷語句中,當被喚醒時,繼續往下執行
while wait()方法在while迴圈語句中,當被喚醒時,要重新判斷while條件
3匿名內部類和區域性內部類在方法體中呼叫區域性變數時(包括區域性變數是物件的引用),要用final修飾,呼叫所在類的成員變數時,不需要final修飾,但是在main方法(靜態方法)中呼叫類的所在類的成員變數時,需要用static修飾
先說下為什麼呼叫區域性變數時要用final修飾?
方法中的類訪問同一個方法中的區域性變數,本來應該是天經地義的,但是這樣會導致編譯程式上實現的困難,因為內部類物件的生命期會超過區域性變數的生命期
1區域性變數的生命期:當該方法被呼叫時,該方法中的區域性變數在棧中被建立,方法呼叫結束時,退棧,區域性變數消亡。
2 內部類生命期,跟其它類一樣,當建立一個內部類物件後,引用為空時,這個內部類才消亡,不會因為它是在方法中定義的,當方法執行完畢它就消亡。
看到這裡出現端倪了,當內部類呼叫所在方法的區域性變數時,當所在的方法呼叫完成,區域性變數就會消亡,但是內部類物件不一定消亡了,(只要它的引用不為空就不會消亡),這時再呼叫該內部類物件的方法或屬性,如果用到該區域性變數,程式就出問題了,內部類物件訪問的屬性不存在,所以用final關鍵字,實際是將區域性變數複製一份(final的作用保證基本資料型別值不變,引用型別引用不變,儘量保證了局部變數和內部類的一致性),用做內部類的成員變數,當局部變數消亡時,還可以用該屬性
還以上面為例,變成三個執行緒輪流列印0-99,將上面程式碼稍微修改下就可以了
public class WaitNotifyTest01 {
public static void main(String[] args) {
Print p = new Print();
Thread t1 = new Thread(p, "執行緒t1");
Thread t2 = new Thread(p, "執行緒t2");
Thread t3 = new Thread(p, "執行緒t3");
t1.start();
t2.start();
t3.start();
}
}
class Print implements Runnable {
static int i = 0;
private boolean flag = true;
public void run() {
while (i < 100) {
synchronized (this) {
if (i >= 100) {// 退出迴圈的判斷條件
break;
}
if (!flag) { // flag為假就讓執行緒等待
try {
this.flag = !flag;// 將flag置反
this.wait(); // 這個執行緒進入等待狀態,下次才能讓另一個執行緒進入等待
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + " : " + (i++));
this.flag = !flag; // 將flag置反,下次才能讓另一個執行緒進入等待
this.notifyAll(); // 喚醒在等待的執行緒
}
}
}
}
}
out:並不是嚴格的輪流列印,但是每一輪三個執行緒各打一個
建立三個執行緒來演示多執行緒的通訊,
package chen_chapter_9;
public class WaitNotifyAllTest01 {
public static void main(String[] args) {
final Print2 p2 = new Print2();// 匿名內部類與區域性內部類訪問成員變數和區域性變數的不同
Thread t1 = new Thread("執行緒t1") {
@Override
public void run() {
while (true) {
p2.print1();
if (p2.stopNum == 100) {
break;
}
}
}
};
Thread t2 = new Thread("執行緒t2") {
@Override
public void run() {
while (true) {
p2.print2();
if (p2.stopNum == 100) {
break;
}
}
}
};
Thread t3 = new Thread("執行緒t3") {
@Override
public void run() {
while (true) {
p2.print3();
if (p2.stopNum == 100) {
break;
}
}
}
};
t1.start();
t2.start();
t3.start();
}
}
class Print2 {
private int i = 0;
private int flag = 1;
volatile int stopNum = 0;
public void print1() {
synchronized (this) {
while (flag != 1) { // 設定條件,每次只讓一個執行緒不等待,等它執行完後再喚醒其他等待的執行緒
try {
this.wait();// 捕獲異常,往上丟擲還要在後面捕獲
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (i < 100) {
System.out.println(Thread.currentThread().getName() + " : " + i++);
} else {
this.stopNum = 100;
}
this.flag = 2;
this.notifyAll();
}
}
public void print2() {
synchronized (this) {
while (flag != 2) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (i < 100) {
System.out.println(Thread.currentThread().getName() + " : " + i++);
} else {
this.stopNum = 100;
}
this.flag = 3;
this.notifyAll();
}
}
public void print3() {
synchronized (this) {
while (flag != 3) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (i < 100) {
System.out.println(Thread.currentThread().getName() + " : " + i++);
} else {
this.stopNum = 100;
}
this.flag = 1;
this.notifyAll();
}
}
}
out:三個輪流列印,且順序不變
分析:分析這塊程式碼,設定flag=1,三個執行緒啟動時,t1,t2,t3,不知道誰會先拿到執行權,這三個執行緒執行一遍
之後,只有t1能執行語句,while語句中t2,t3進入等待狀態,當t1在同步程式碼塊的內容執行完以後設定flag=2,
呼叫notifyAll方法,喚醒t2,t3,這時三個執行緒又開始搶cpu控制權,但因為flag=2,這時只有t2執行緒能執
行,while語句中將t1,t3設定成等待狀態,t2執行完後將設定flag=3,呼叫notifyAll方法,將t1,t3喚醒,三
個執行緒又開始搶cpu控制權,但是因為flag=3,所以只能t3執行緒執行,while語句中讓t1,t2變成等待狀態,t3執
行完同步程式碼塊內容後,設定flag=1,又回到剛開始的迴圈,在這裡設定了個變數stopNum,是因為執行緒中的run
方法是死迴圈,要設定一個停止條件
sleep wait notify的區別
sleep:睡眠一段時間不釋放鎖,醒來後繼續執行之前的任務,不釋放鎖
wait:等待後,釋放物件鎖,只能被別的執行緒喚醒notify,喚醒後接著執行之前未完的任務,注意在while和if語句塊中的區別
notify不釋放鎖