Java併發(二)線程間通信-生產者消費者模型

1 線程通信 - wait、notify、notifyAll

1.1 wait方法

public final void wait() throws InterruptedException {
	wait(0);
}
public final native void wait(long timeout) throws InterruptedException;

和上一篇文章一樣,咱們先看官方文檔:

Causes the current thread to wait until another thread invokes the {@link java.lang.Object#notify()} method or the {@link java.lang.Object#notifyAll()} method for this object, or some other thread interrupts the current thread, or a certain amount of real time has elapsed.

The current thread must own this object’s monitor. The thread releases ownership of this monitor and waits until another thread notifies threads waiting on this object’s monitor to wake up either through a call to the notify method or the notifyAll method. The thread then waits until it can re-obtain ownership of the monitor and resumes execution.

As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:

synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout, nanos);
… //Perform action appropriate to condition
}

This method should only be called by a thread that is the owner of this object’s monitor. See the notify method for a description of the ways in which a thread can become the owner of a monitor.

核心內容歸納如下:

1、wait會導致當前線程等待,直到被其他線程調用notify、notifyAll方法喚醒。

注意:

  • 通常我們說“對象調用方法”,但是在多線程中喜歡說“線程調用方法”。實際上線程中的對象在調用方法,都可以理解爲線程在調用方法。
  • wait方法是由鎖對象來調用的,也就是monitor,而且喚醒線程的notify方法也是由同一個鎖對象調用的。

2、當前線程必須擁有此對象的monitor,也就是對象鎖。當前線程調用wait方法後,會釋放這個對象鎖並等待,直到另外一個線程調用notify方法去喚醒。

注意:當前線程被喚醒不代表立即獲取了對象的monitor,只有等另一線程調用完notify()並退出synchronized塊,釋放對象鎖後,當前線程纔可獲得鎖執行

3、wait方法應始終在循環中使用。

4、wait方法只能被擁有對象鎖的線程調用,即wait方法只能在同步方法或同步語句塊中調用。

1.2 notify方法

public final native void notify();

官方解釋:

Wakes up a single thread that is waiting on this object’s monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation. A thread waits on an object’s monitor by calling one of the wait methods.

The awakened thread will not be able to proceed until the current thread relinquishes the lock on this object. The awakened thread will compete in the usual manner with any other threads that might be actively competing to synchronize on this object; for example, the awakened thread enjoys no reliable privilege or disadvantage in being the next thread to lock this object.

This method should only be called by a thread that is the owner of this object’s monitor. A thread becomes the owner of the object’s monitor in one of three ways:

  • By executing a synchronized instance method of that object.
  • By executing the body of a synchronized statement that synchronizes on the object.
  • For objects of type Class, by executing a synchronized static method of that class.

Only one thread at a time can own an object’s monitor.

核心內容解釋如下:

1、調用notify方法,可喚醒在對象鎖(monitor)上等待的單個線程。如果有很多線程等待此對象鎖,則隨機選擇一個去喚醒。

2、在當前線程放棄對該對象的鎖定之前,被喚醒的線程將無法繼續運行。

3、notify()方法只應由作爲此對象鎖(monitor)所有者的線程調用。即notify方法只能在同步方法或同步語句塊中調用。

4、每一次只有一個線程可以擁有對象的鎖(monitor)。

1.3 notifyAll方法

notifyAll方法與notify方法基本相似,只是notifyAll方法,用於喚醒在該對象鎖(monitor)上等待的所有線程

2 生產者 - 消費者模型多種實現方式

生產者消費者問題是研究多線程程序繞不開的經典問題之一。生產者-消費者模型,簡單概括如下:

  • 生產者持續生產,直到緩衝區滿,阻塞;緩衝區不滿後,生產者繼續生產
  • 消費者持續消費,直到緩衝區空,阻塞;緩衝區不空後,消費者持續消費
  • 生產者可以有多個,消費者可以有多個

可通過如下條件驗證模型的正確性:

  • 同一產品的消費行爲一定發生在生產行爲之後
  • 任意時刻,緩衝區的大小不得小於0,也不得大於其最大容量

在實現模型之前,首先構思一下基本的類結構。

在這裏插入圖片描述
主要設計如下:

  • Storage(倉庫)接口,接口中設計了produce()、consume()方法。由於後期produce方法、consumer方法都會改動。所以在設計的時候將可能發生的變化集中到一個類中,不影響原有的構架設計,同時無需修改其他業務代碼
  • Storage實現類,實現具體的produce()、consume()方法
  • Producer(生產者)類
  • Consumer(消費者類)類

2.1 wait、notify實現

/**
 * 倉庫接口,象徵着生產者和消費者中間的隊列
 * @Author: cherry
 * @Date: Created in 2018/9/27 15:36
 */
public interface Storage {

    /**
     * 生產行爲
     * @param num 生產商品的數量
     */
    void produce(int num);

    /**
     * 消費行爲
     * @param num 消費商品的數量
     */
    void consume(int num);
}
/**
 * Storage接口的實現類,produce、consume方法採用wait、notify實現
 * @Author: cherry
 * @Date: Created in 2018/9/27 15:41
 */
public class StorageWaitNotifyImpl implements Storage {
    /**
     * 隊列最大容量
     */
    public static final int MAX_SIZE = 100;

    /**
     * 倉庫中,存放產品的容器。也可以用作同步對象鎖
     */
    private LinkedList<Object> queue = new LinkedList<>();


    @Override
    public void produce(int num) {
        synchronized (queue){
            //判斷緩存隊列大小是否會超出最大容量
            while(queue.size() + num > MAX_SIZE){
                System.out.println("要生產的數量:"+ num + ",現庫存量:" + queue.size() + ",總容量:100,暫停生產!");
                try {
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //線程被喚醒或未超出最大容量
            for (int i = 0; i < num; i++) {
                queue.add(new Object());
            }

            //生產者生產完畢,喚醒消費者
            queue.notifyAll();
            System.out.println("已生產數量:"+ num + ",現庫存量:" + queue.size());
        }
    }

    @Override
    public void consume(int num) {
        synchronized (queue){
            //判斷緩存隊列大小是否小於0
            while(queue.size() - num < 0){
                System.out.println("要消費的數量:"+ num + ",現庫存量:" + queue.size() + ",暫停消費!");
                try {
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //線程被喚醒或隊列不爲空
            for (int i = 0; i < num; i++) {
                queue.remove();
            }

            //消費者消費完畢,喚醒生產者
            queue.notifyAll();
            System.out.println("已消費數量:"+ num + ",現庫存量:" + queue.size());
        }
    }
}
/**
 * 生產者
 * @Author: cherry
 * @Date: Created in 2018/9/27 15:47
 */
public class Producer implements Runnable {
    /**
     * 聚合倉庫對象,produce方法中會調用倉庫對象的produce方法
     */
    private Storage storage;

    /**
     * 生產數量
     */
    private int num;

    public Producer(){

    }

    public Producer(Storage storage,int num){
        this.storage = storage;
        this.num = num;
    }


    @Override
    public void run() {
        this.produce(num);
    }

    /**
     * 生產者produce方法中調用storage.produce方法
     * @param num 生產數量
     */
    private void produce(int num){
        this.storage.produce(num);
    }

    public Storage getStorage() {
        return storage;
    }

    public void setStorage(Storage storage) {
        this.storage = storage;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }
}
/**
 * 消費者
 * @Author: cherry
 * @Date: Created in 2018/9/27 16:02
 */
public class Consumer implements Runnable {
    /**
     * 聚合倉庫對象,this.consume()方法中會調用倉庫對象的consume方法
     */
    private Storage storage;

    /**
     * 消費數量
     */
    private int num;

    public Consumer(){

    }

    public Consumer(Storage storage,int num){
        this.storage = storage;
        this.num = num;
    }

    @Override
    public void run() {
        this.consume(num);
    }

    private void consume(int num){
        this.storage.consume(num);
    }

    public Storage getStorage() {
        return storage;
    }

    public void setStorage(Storage storage) {
        this.storage = storage;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }
}
/**
 * @Author: cherry
 * @Date: Created in 2018/9/27 16:37
 */
public class TestQueue {
    public static void main(String[] args){
        Storage storage = new StorageWaitNotifyImpl();

        //初始化生產者
        Producer producer1 = new Producer(storage,30);
        Producer producer2 = new Producer(storage,40);
        Producer producer3 = new Producer(storage,50);
        Producer producer4 = new Producer(storage,60);
        Producer producer5 = new Producer(storage,70);


        //初始化消費者
        Consumer consumer1 = new Consumer(storage,30);
        Consumer consumer2 = new Consumer(storage,40);
        Consumer consumer3 = new Consumer(storage,50);
        Consumer consumer4 = new Consumer(storage,60);
        Consumer consumer5 = new Consumer(storage,70);

        new Thread(producer1).start();
        new Thread(producer2).start();
        new Thread(producer3).start();
        new Thread(producer4).start();
        new Thread(producer5).start();
        new Thread(consumer1).start();
        new Thread(consumer2).start();
        new Thread(consumer3).start();
        new Thread(consumer4).start();
        new Thread(consumer5).start();
    }

}

運行結果:

已生產數量:30,現庫存量:30
已生產數量:40,現庫存量:70
要生產的數量:50,現庫存量:70,總容量:100,暫停生產!
要生產的數量:60,現庫存量:70,總容量:100,暫停生產!
要生產的數量:70,現庫存量:70,總容量:100,暫停生產!
已消費數量:30,現庫存量:40
已消費數量:40,現庫存量:0
已生產數量:70,現庫存量:70
要生產的數量:60,現庫存量:70,總容量:100,暫停生產!
已消費數量:60,現庫存量:10
已生產數量:50,現庫存量:60
要消費的數量:70,現庫存量:60,暫停消費!
要生產的數量:60,現庫存量:60,總容量:100,暫停生產!
已消費數量:50,現庫存量:10
已生產數量:60,現庫存量:70
已消費數量:70,現庫存量:0

2.2 await、signal實現

在實現之前,咱們分析一個簡單的性能問題:在2.1小節中的queue.notifyAll(),該方法會喚醒所有等待queue對象鎖的線程,也就是說如果生產者剛剛生產完畢,調用notifyAll()方法想喚醒的是消費者,結果卻喚醒了生產者,被喚醒的生產者(因爲超出容量)又將等待,這樣效率會很低!

如何解決此效率問題呢?後面會有解釋,咱們先往下走。

在JDK5.0之後,java.util.concurrent包提供了Lock && Condition

Lock實現提供了比使用synchronized方法和語句塊可獲得的更廣泛的鎖定操作,它能以更優雅的方式處理線程同步問題。利用lock()unlock()來鎖定需同步的語句。當然Lock的強大之處不僅僅在於此,讀寫鎖(ReadWriteLock)更彰顯其強大。讀寫鎖(ReadWriteLock)可實現:讀與寫互斥、寫與寫互斥、而讀與讀不互斥。

Condition用來替代傳統的Object的wait()、notify()實現線程間的通信。在Condition中,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll(),傳統線程的通信方式,Condition都可以實現,這裏注意,Condition是被綁定到Lock上的,要創建一個Lock的Condition必須用newCondition()方法。調用Condition的await()、signal()和signalAll()方法,都必須在lock保護之內,就是說必須在lock.lock()和lock.unlock之間纔可以使用。

現在要使用Lock、以及Condition的await、signal方法去改寫模型。那麼只需重新實現Storage接口即可,其他代碼幾乎不需要改變。

/**
 * Storage的實現類。採用Lock、多個Condition去實現同步
 * @Author: cherry
 * @Date: Created in 2018/9/27 19:44
 */
public class StorageConditionImpl implements Storage {

    /**
     * 隊列最大容量
     */
    public static final int MAX_SIZE = 100;

    /**
     * 倉庫中,存放產品的容器
     */
    private LinkedList<Object> queue = new LinkedList<>();

    private Lock lock = new ReentrantLock();

    /**
     * 作爲生產者使用的同步鎖
     */
    private Condition notFull = lock.newCondition();

    /**
     * 作爲消費者使用的同步鎖
     */
    private Condition notEmpty = lock.newCondition();

    @Override
    public void produce(int num) {
        lock.lock();
        try{
            //判斷緩存隊列大小是否會超出最大容量
            while(queue.size() + num > MAX_SIZE){
                System.out.println("要生產的數量:"+ num + ",現庫存量:" + queue.size() + ",總容量:100,暫停生產!");
                try {
                    //生產者等待
                    notFull.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //線程被喚醒或未超出最大容量
            for (int i = 0; i < num; i++) {
                queue.add(new Object());
            }

            //生產者生產完畢,喚醒消費者
            notEmpty.signalAll();
            System.out.println("已生產數量:"+ num + ",現庫存量:" + queue.size());
        }finally {
            lock.unlock();
        }
    }

    @Override
    public void consume(int num) {
        lock.lock();
        try{
            //判斷緩存隊列大小是否小於0
            while(queue.size() - num < 0){
                System.out.println("要消費的數量:"+ num + ",現庫存量:" + queue.size() + ",暫停消費!");
                try {
                    //消費者等待
                    notEmpty.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //線程被喚醒或隊列不爲空
            for (int i = 0; i < num; i++) {
                queue.remove();
            }

            //消費者消費完畢,喚醒生產者
            notFull.signalAll();
            System.out.println("已消費數量:"+ num + ",現庫存量:" + queue.size());
        }finally {
            lock.unlock();
        }
    }
}

測試結果:

已生產數量:30,現庫存量:30
已生產數量:40,現庫存量:70
要生產的數量:50,現庫存量:70,總容量:100,暫停生產!
要生產的數量:60,現庫存量:70,總容量:100,暫停生產!
要生產的數量:70,現庫存量:70,總容量:100,暫停生產!
已消費數量:30,現庫存量:40
已生產數量:50,現庫存量:90
已消費數量:50,現庫存量:40
已生產數量:60,現庫存量:100
要生產的數量:70,現庫存量:100,總容量:100,暫停生產!
已消費數量:40,現庫存量:60
要生產的數量:70,現庫存量:60,總容量:100,暫停生產!
已消費數量:60,現庫存量:0
已生產數量:70,現庫存量:70
已消費數量:70,現庫存量:0

消費者等待使用notEmpty.await(),而喚醒生產者使用notFull.signalAll()。這樣就解決之前使用notifyAll的性能問題。使用notEmpty notFull兩個Condition條件可以使得當隊列存滿時,那麼阻塞的肯定是生產者線程,喚醒的肯定是消費者線程,相反,當隊列爲空時,阻塞的肯定是消費者線程,喚醒的肯定是生產者線程

2.3 BlockingQueue實現

BlockingQueue是JDK5.0的新增內容,它是一個已經在內部實現了同步的隊列,實現方式採用的是await()、signal()方法。它用於阻塞的操作是put()和take()方法。

  • put():類似於我們上面的生產者線程,容量達到最大時,自動阻塞。
  • take():類似於我們上面的消費者線程,容量爲0時,自動阻塞。

實現代碼非常簡單,跟以往一樣,我們只需要重新實現Storage接口即可:

/**
 * @Author: cherry
 * @Date: Created in 2018/9/27 21:25
 */
public class StorageBlockQueueImpl implements Storage {
    /**
     * 隊列最大容量
     */
    public static final int MAX_SIZE = 100;

    /**
     * 阻塞隊列
     */
    private LinkedBlockingQueue<Object> queue = new LinkedBlockingQueue<>(MAX_SIZE);

    @Override
    public void produce(int num) {
        for (int i = 0; i < num; i++) {
            try {
                //自動阻塞
                queue.put(new Object());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void consume(int num) {
        for (int i = 0; i < num; i++) {
            try {
                //自動阻塞
                queue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3 參考博客

生產者-消費者問題的多種Java實現方式

Java線程(九):Condition-線程通信更高效的方式

Java線程(八):鎖對象Lock-同步問題更完美的處理方式

Java併發編程:線程間協作的兩種方式:wait、notify、notifyAll和Condition

Java 實現生產者 – 消費者模型

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