1. 程式人生 > >JAVA多執行緒wait與notify詳細解析(由生產者和消費者案例引出)

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方法才能喚醒