線程操作的實例

在線程操作中有個經典的案例程序,即生產者和消費者問題,生產者不斷生產,消費者不斷消費生產者生產的產品。
生產者生產出的信息方法一個區域之中,消費者從區域中將數據取出來,但是本程序中因爲牽扯到線程運行的不確定性,所以會存在兩個問題:
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
張三-->教師
。。。。。

從程序運行的效果來看。一個生產者生產完成,就等待一個消費者取出,消費者取出一個就等待生產者進行生產,這樣就避免了重複生產和重複取出的問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章