1. 程式人生 > >java 多執行緒之通訊

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不釋放鎖