Thinking in Java---執行緒通訊+三種方式實現生產者消費者問題

acm_lkl發表於2020-04-04

前面講過執行緒之間的同步問題;同步問題主要是為了保證對共享資源的併發訪問不會出錯,主要的思想是一次只讓一個執行緒去訪問共享資源,我們是通過加鎖的方法實現。但是有時候我們還需要安排幾個執行緒的執行次序,而在系統內部執行緒的排程是透明的,沒有辦法準確的控制執行緒的切換。所以Java提供了一種機制來保證執行緒之間的協調執行,這也就是我們所說的執行緒排程。在下面我們會介紹三種用於執行緒通訊的方式,並且每種方式都會使用生產者消費者問題進行檢驗。

一。使用Object類提供的執行緒通訊機制
Object類提供了wait(),notify(),notifyAll()三個方法進行執行緒通訊。這三個方法都必須要由同步監視器物件來呼叫,具體由分為以下兩種情況:
1)對於使用synchronized修飾的同步方法,因為該類的預設例項(this)就是同步監視器,所以可以在同步方法中直接呼叫這三個方法。
2)對於使用synchronized修飾的同步程式碼塊,同步監視器是synchronized後面括號裡的物件,所以必須使用該物件呼叫這三個方法。
也就是說,這三個方法只能用於synchronized做同步的執行緒通訊。對著三個方法的具體解釋如下:
wait():導致當前執行緒等待,直到其它執行緒呼叫該同步監視器的notify()或notifyAll()方法來喚醒該執行緒。該wait()方法還可以傳入一個時間引數,這時候等到指定時間後就會自動甦醒。呼叫wait()方法的當前執行緒會釋放對該同步監視器的鎖定。
notify():喚醒在此同步監視器上等待的單個執行緒。如果當前有多個執行緒在等待,則隨機選擇一個。注意只有當前執行緒放棄對該同步監視器的鎖定以後(使用了wait()方法),才可以執行被喚醒的執行緒。
notifyAll():喚醒在此同步監視器上等待的所有執行緒。同樣只要在當前執行緒放棄對同步監視器的鎖定之後,才可以執行被喚醒的執行緒。

使用這種通訊機制模擬的生產者消費者問題如下:

package lkl1;

///生產者消費者中對應的緩衝區
//生產者可以向緩衝區中加入資料,消費者可以消耗掉緩衝區中的資料
//注意到緩衝區是限定了大小的,所以使用迴圈佇列的思想進行模擬

public class Buffer {
//根據迴圈佇列的思想,如果out==in,則表示當前緩衝區為空,不可以進行消費
    //如果(in+1)%n==out,則表示當前緩衝區為滿,不可以進行生產(這樣會浪費一個空間)

    private int n; ///緩衝區大小
    private int num; //當前元素個數

    //定義一個大小為n的緩衝區
    private int buffer[];
    //表示當前可以放置資料的位置,初始為0
    private int in=0;
    //表示當前可以讀取資料的位置,初始為0
    private int out=0;

    Buffer(int n){
        this.n=n;
        buffer=new int[n];
        num=0;
    }
    //下面是生產和消費的方法

    //生產操作,向緩衝區中加入一個元素x
    public synchronized void product(int x){

        try{
            if((in+1)%n==out){
                wait(); //如果緩衝區已滿,則阻塞當前執行緒
            }
            else{
                buffer[in]=x;
                in=(in+1)%n;
                System.out.println(Thread.currentThread().getName()+"生產一個元素: "+x);
                num++;
                System.out.println("當前元素個數為: "+num);
                notifyAll(); //喚醒等待當前同步資源監視器的執行緒
            }
        }
        catch(InterruptedException ex){
            ex.printStackTrace();
        }
    }

    ///消費操作,一次取出一個元素
    public synchronized void comsumer(){

        try{
            if(in==out){ //如果緩衝區為空,阻塞當前執行緒
                wait();
            }
            else{
                int xx=buffer[out];
                out=(out+1)%n;
                num--;
                System.out.println(Thread.currentThread().getName()+"消費了一個元素: "+xx);
                System.out.println("當前元素個數為: "+num);
                notifyAll();
            }
        }
        catch(InterruptedException ex){
            ex.printStackTrace();
        }
    }
}
package lkl1;

import java.util.Random;

//生產者執行緒
//會不斷的往緩衝區中加入元素
public class Product extends Thread{

    //當前執行緒操作的緩衝區物件
    private Buffer buffer;
    private Random rand;
    Product(){}
    Product(Buffer buffer){
        this.buffer=buffer;
        rand=new Random();
    }
    public void run(){
        while(true){
            //向緩衝區中新增一個隨機數
            buffer.product(rand.nextInt(100));
        }
    }
}
package lkl1;

//生產者執行緒
public class Consumer extends Thread{

    private Buffer buffer;
    Consumer(){}
    Consumer(Buffer buffer){
        this.buffer=buffer;
    }
    public void run(){  
        while(true){ //每次都消耗掉緩衝區中的一個元素
            buffer.comsumer();
        }
    }
}
package lkl1;
//測試
public class BufferTest {

    public static void main(String[] args){
        Buffer buffer = new Buffer(10); 

        //一個生產者,多個消費者
        new Product(buffer).start(); 
        new Consumer(buffer).start();
        new Consumer(buffer).start();
    }
}

二。使用Condition控制執行緒通訊
前面我們講同步方式的時候,除了synchronized關鍵字,還講了可以使用Lock進行顯示的加鎖。在使用Lock物件時,是不存在隱式的同步監視器的,所以也就不能使用上面的執行緒通訊方式了。其實在使用Lock物件來保證同步時,Java提供了一個Condition類來保持協調,使用Condition類可以讓那些已經得到Lock物件卻無法繼續執行的執行緒釋放Lock物件。
Condition提供了三個方法:await(),signal(),signallAll();這三個方法和Object物件的三個方法的基本用法是一樣的。其實我們可以這樣認為,Lock物件對應了我們上面講的同步方法或同步程式碼塊,而Condition物件對應了我們上面講的同步監視器。還要注意的是,Condition例項被繫結在一個Lock物件上,要獲得指定Lock的Condition例項,需要呼叫Lock物件的newCondtion()方法即可。
下面使用Lock和Condition的組合來實現生產者消費者問題。可以看到程式碼基本和上面是一樣的。

package lkl1;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

///生產者消費者中的緩衝區
//由一個陣列代表,生產者可以向緩衝區中加入元素,消費者可以從緩衝區中取走元素
//如果緩衝區滿,則生產者不能向緩衝區加入元素;如果緩衝區空,則消費者不能消費元素
//下面的程式中in表示生產者可以加入資料的位置,out表示消費者可以消費資料的位置
//in和out都會初始化為0,我們定義in==out表示緩衝區為空;(in+1)%n==out
//表示緩衝區滿,但是這種判滿的方式是要浪費一個空間的。

//上個例子中使用了synchronized關鍵字保證對緩衝區的操作的同步。
//現在需要採用Lock和Condition類進行同步的控制.
public class Buffer1 {

    private final Lock lock=new ReentrantLock();
    private final Condition con=lock.newCondition();
    private int n;
    private int buffer1[];
    private int in;
    private int out;
    private int cnt; ///記錄當前緩衝區中元素個數
    Buffer1(){}
    Buffer1(int n){
        this.n=n;
        buffer1=new int[n];
        in=out=cnt=0;
    }

   //生產方法,加入元素x
   public void product(int x){
       lock.lock(); //加鎖
       try{
           if((in+1)%n==out){ //如果緩衝區滿,則阻塞當前執行緒
               con.await();
               //con.signalAll();
           }
           else{
               buffer1[in]=x;
               in=(in+1)%n;
               cnt++;
               System.out.println(Thread.currentThread().getName()+"向緩衝區中加入元素:"+x);
               System.out.println("當前緩衝區中的元素個數為: "+cnt);
               con.signalAll(); //喚醒其它執行緒
           }
       }
       catch(InterruptedException ex){
           ex.printStackTrace();
       }
       finally{ ///使用finally語句保證鎖能正確釋放
           lock.unlock();
       }
   }

   //消費方法,取走緩衝區中的一個元素
   public int consumer(){
       int x=0;
       lock.lock();
       try{
           if(in==out){ //如果緩衝區空,則阻塞當前執行緒
               con.await();
           }
           else{
               x=buffer1[out];
               System.out.println(Thread.currentThread().getName()+"消費元素: "+x);
               out=(out+1)%n;
               cnt--;
               System.out.println("當前元素個數為: "+cnt);
               con.signalAll(); //喚醒其它執行緒
           }
       }
       catch(InterruptedException ex){
           ex.printStackTrace();
       }
       finally{
           lock.unlock();
       }
       return x;
   }
}
package lkl1;

import java.util.Random;

//消費者執行緒
public class Consumer1 extends Thread{

    private Random rand=new Random();
    private Buffer1 buffer1; //對應的緩衝區
    Consumer1(Buffer1 buffer1){
        this.buffer1=buffer1;
    }
    public void run(){
        while(true){
            buffer1.consumer();
            try{    ///在消費者執行緒中加一個sleep語句,可以更好的體現執行緒之間的切換
                sleep(50);
            }
            catch(Exception x){
                x.printStackTrace();
            }
        }
    }
}
package lkl1;

import java.util.Random;

//生產者執行緒
public class Product1 extends Thread{

    private Random rand = new Random();
    private Buffer1 buffer1;
    Product1(Buffer1 buffer1){
        this.buffer1=buffer1;
    }
    public void run(){
        while(true){
            int x;
            x=rand.nextInt(100);
            buffer1.product(x);
        }
    }
}
package lkl1;

///Buffer1測試
//啟動一個生產者執行緒,兩個消費者執行緒
public class Buffer1Test {

    public static void main(String[] args) throws Exception{
        Buffer1 buffer1 = new Buffer1(10);
        new Product1(buffer1).start(); 
        new Consumer1(buffer1).start();
        new Consumer1(buffer1).start();
    }
}

三。使用阻塞佇列(BlockingQueue)來控制執行緒通訊
Java5提供了一個BlockingQueue介面,這個介面也是屬於佇列的子介面,但是他主要的作用還是用來進行執行緒通訊,而不是當成佇列用。BlockingQueue的特徵是:當生產者執行緒試圖向BlockingQueue中加入元素時,如果該佇列已滿,則該執行緒被阻塞;當消費者執行緒試圖從BlockingQueue中取出元素時,如果該佇列為空,則該執行緒會被阻塞。
這樣程式的兩個執行緒通過交替的向BlocingQueue中放入元素,取出元素,即可很好的控制執行緒的通訊。當然也不是所有的BlockingQueue的方法都支援阻塞操作的。
BlockingQueue提供了以下兩個支援阻塞的方法:
put(E e):嘗試把E元素放入BlockingQueue中,如果該佇列的元素已滿,則阻塞該執行緒。
take():嘗試從BlockingQueue的頭部取出元素,如果該佇列的元素為空,則阻塞該執行緒。
其它的Queue對應的方法,也都是支援的,但是在上面的情況下操作,不會阻塞,而是會返回false或丟擲異常。
常用的BlockingQueue的實現類有以下幾種:
ArrayBlockingQueue:基於陣列實現的BlockingQueue佇列
LinkedBlockingQueue:基於連結串列實現的BlockingQueue佇列
PriorityBlockingQueue:優先佇列對應的阻塞佇列
SynchronizedQueue:同步佇列,對該佇列的存取必須交替進行

因為阻塞佇列本身就支援生產者消費者模式,所以用阻塞佇列來實現生產者消費者問題就很簡單了。

package lkl;

import java.util.concurrent.BlockingQueue;

///消費者執行緒
public class Consumer extends Thread{

    private BlockingQueue<String>bq;
    public Consumer(BlockingQueue<String> bq){
        this.bq=bq;
    }
    public void run(){
        while(true){
            System.out.println(getName()+"消費者準備消費集合元素!");
            try{
                Thread.sleep(200);
                //嘗試取出元素,如果佇列以空,則執行緒阻塞
                bq.take();
            }
            catch(Exception ex){
                ex.printStackTrace();
            }
            System.out.println(getName()+"消費完成: "+bq);
        }
    }
}
package lkl;

import java.util.concurrent.BlockingQueue;

//生產者執行緒
public class Producer extends Thread{

    private BlockingQueue<String>bq;
    public Producer(BlockingQueue bq){
        this.bq=bq;
    }
    public void run(){
        String[] strArr = new String[]{
                "Java","Struts","Spring"
        };
        for(int i=0;i<99999999;i++){
            System.out.println(getName()+"生產者準備生產集合元素!");
            try{
                Thread.sleep(200);
                //嘗試放入元素,如果佇列已滿,則執行緒會被阻塞
                bq.put(strArr[i%3]);
            }
            catch(Exception ex){
                ex.printStackTrace();
            }
            System.out.println(getName()+"生產完成: "+bq);
        }
    }
}
package lkl;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class BlockingQueueTest2 {

    public static void main(String[] args){

        //建立一個容量為1的BlockingQueue
        BlockingQueue<String> bq=new ArrayBlockingQueue<>(1);

        //啟動3個生產者執行緒
        new Producer(bq).start();
        new Producer(bq).start();
        new Producer(bq).start();

        //啟動一個消費者執行緒
        new Consumer(bq).start();
    }
}

相關文章