在線程操作中有個經典的案例程序,即生產者和消費者問題,生產者不斷生產,消費者不斷消費生產者生產的產品。
生產者生產出的信息方法一個區域之中,消費者從區域中將數據取出來,但是本程序中因爲牽扯到線程運行的不確定性,所以會存在兩個問題:
1. 假設生產者線程剛向數據空間添加了信息的名稱,還沒有加入該信息的內容,程序就切換到了消費者線程,消費者線程將把信息的名稱和上一個信息的內容聯繫在一起。
2. 生產者放了若干次的數據,消費者纔開始取數據,或者是,消費者取完一個數據後,還沒等到生產者放入新的數據,又重複取出已經取出的數據。
- 程序的基本實現
因爲程序中生產者不斷生產的是信息,而消費者不斷取出的也是信息,所以定義一個保存信息的類Info.java
【Info.java】
class Info
{
private String name = "張三";
private String content= "教師";
public String getName(){
return name;
}
public void setName(String name){
this.name=name;
}
public String getContent(){
return content;
}
public void setContent(String content){
this.content=content;
}
}
Info類的組成非常簡單,只包含了用於保存信息名稱的name屬性和用於保存信息生產者的content屬性,因爲生產者和消費者要同時操作一個空間的內容,所以生產者和消費者分別實現Runnable接口,並接收Info類的引用。
【生成者】
class Producer implements Runnable
{
private Info info=null;
public Producer(Info info){
this.info=info;
}
public run(){
boolean flag=false;
for (int i=0;i<50 ;i++ )
{
if (flag)
{
this.info.setName("張三");
try
{
Thread.sleep(90);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
this.info.setContent("教師");
flag=false;
}else
{
this.info.setName("zhangsan");
try
{
Thread.sleep(90);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
this.info.setContent("www.zhangsan.com");
flag=true;
}
}
}
}
在生產者類的構造方法中傳入了Info類的實例化對象,然後在run()方法中循環50次以產生信息的具體內容,此外爲了讓讀者更好的發現問題,在線程中加入了延遲操作。
【消費者】
class Consumer implements Runnable
{
private Info info=null;
public Consumer (Info info){
this.info=info;
}
public void run(){
for (int i=0;i<50 ;i++ )
{
try
{
Thread.sleep(90);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(this.info.getName()+"-->"+this.info.getContent());
}
}
}
在消費者線程類中同樣也接收了一個info對象的引用,並採用循環的方式取出50次信息並輸出。
【測試程序】
public class ThreadCaseDemo01
{
public static void main(String args[])
{
Info info=new Info();
Producer pro=new Producer(info);
Consumer con=new Consumer(info);
new Thread(pro).start();
new Thread(con).start();
}
}
運行結果:
張三-->www.zhangsan.com
zhangsan-->教師
張三-->www.zhangsan.com
zhangsan-->教師
張三-->www.zhangsan.com
zhangsan-->教師
張三-->www.zhangsan.com
zhangsan-->教師
張三-->www.zhangsan.com
zhangsan-->教師
張三-->www.zhangsan.com
zhangsan-->教師
張三-->www.zhangsan.com
。。。。。
zhangsan-->教師
張三-->www.zhangsan.com
張三-->教師
張三-->www.zhangsan.com
張三-->教師
。。。。
從運行結果來看前面所提到的問題都出現了,下面先來解決第一個問題。
- 問題解決1——加入同步
如果要爲操作加入同步,則可以通過定義同步方法的方式完成,即將設置名稱和姓名定義成一個同步方法。
【修改Info類】
在info類中加入;
public synchronized void set(String name,String content){
this.setName(name);
try
{
Thread.sleep(300);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
this.setContent(content);
}
public synchronized void get(){
try
{
Thread.sleep(300);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(this,getName()+"-->"+this.getContent());
}
在類中加入了一個set()和get()方法,並使用synchronized關鍵字進行聲明,因爲現在不希望直接調用getter及setter方法,所以修改生產者和消費者類中的代碼就行了
【修改生產者】
class Producer implements Runnable
{
private Info info=null;
public Producer(Info info){
this.info=info;
}
public void run(){
boolean flag=false;
for (int i=0;i<50 ;i++ )
{
if (flag)
{
this.info.set("張三","教師");
flag=false;
}else
{
this.info.set("zhangsan","www.zhangsan.com");
flag=true;
}
}
}
};
消費者的修改同理,不再給出。
運行部分結果:
張三-->教師
zhangsan-->www.zhangsan.com
張三-->教師
張三-->教師
zhangsan-->www.zhangsan.com
zhangsan-->www.zhangsan.com
zhangsan-->www.zhangsan.com
.....
從上面的輸出結果可以看出信息錯亂的問題已經得到解決,但是依然存在重複讀取的問題,此時就可以使用Object類來幫忙了。
- Object類對線程的支持——等待與喚醒
從前面的學習中知道Object類是所有類的父類,在此類中有以下幾種方法是對線程操作有所支持的。
方法 | 類型 | 描述 |
---|---|---|
public final void wait() throws InterruptedException | 普通 | 線程等待 |
public final void wait(long timeout) throws InterruptedException | 普通 | 線程等待,並指定等待的最大時間,毫秒 |
public final void wait(long timeout,int nanos) throws InterruptedException | 普通 | 線程等待,並指定等待的最長毫秒和納秒 |
public final void notify() | 普通 | 喚醒一個等待的線程 |
public final void notifyAll() | 普通 | 喚醒全部等待的線程 |
從表中可以看出,可以將一個線程設置爲等待狀態,但是對於喚醒的操作卻由notify()和notifyAll()兩個方法。一般來說,所有等待的線程依照順序進行排序,如果現在使用了notify()方法,則會喚醒第一個等待的線程執行,而如果使用了notifyAll()方法,哪個線程的優先級越高,哪個線程就有可能先執行。
- 問題解決2——加入等待與喚醒
如果想讓生產者不重複生產,消費者不重複取走,則可以增加一個標誌位,假設標誌位爲boolean型變量,如果標誌位的內容爲true,則表示可以生產,但是不能取走,此時線程執行到了消費者線程則應該等待,如果標誌位的內容爲false,則表示可以取走,但是不能生產,如果生產者線程運行,則消費者線程應該等待。
要完成上述功能,直接在Info類中進行修改就行。在Info類中加入標誌位,並通過判斷標誌位來完成等待與喚醒的操作。
【修改Info類】
class Info
{
private String name = "張三";
private String content= "教師";
private boolean flag=false;
public synchronized void set(String name,String content){
if (!flag)
{
try
{
super.wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
this.setName(name);
try
{
Thread.sleep(300);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
this.setContent(content);
flag=false;
super.notify();
}
public synchronized void get(){
if (flag)
{
try
{
super.wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
try
{
Thread.sleep(300);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(this.getName()+"-->"+this.getContent());
flag =true;//可以進行生產
super.notify();
}
public String getName(){
return name;
}
public void setName(String name){
this.name=name;
}
public String getContent(){
return content;
}
public void setContent(String content){
this.content=content;
}
};
運行效果:
張三-->教師
zhangsan-->www.zhangsan.com
張三-->教師
zhangsan-->www.zhangsan.com
張三-->教師
zhangsan-->www.zhangsan.com
張三-->教師
。。。。。
從程序運行的效果來看。一個生產者生產完成,就等待一個消費者取出,消費者取出一個就等待生產者進行生產,這樣就避免了重複生產和重複取出的問題