九、生產者與消費者模式

abc十號發表於2020-07-12

生產者消費者模式

  • 生產者消費者模式是程式設計中非常常見的一種設計模式,被廣泛運用在解耦、訊息佇列等場景。

  • 使用生產者消費者模式通常需要在兩者之間增加一個阻塞佇列作為媒介,有了媒介之後就相當於有了一個緩衝,平衡了兩者的能力。

  • 整體如上圖所示,最上面是阻塞佇列,右側的 1 是生產者執行緒,生產者在生產資料後將資料存放在阻塞佇列中,左側的 2 是消費者執行緒,消費者獲取阻塞佇列中的資料。

  • 而中間的 3 和 4 分別代表生產者消費者之間互相通訊的過程,因為無論阻塞佇列是滿還是空都可能會產生阻塞,阻塞之後就需要在合適的時機去喚醒被阻塞的執行緒。

  • 那麼什麼時候阻塞執行緒需要被喚醒呢?有兩種情況。

  • 第一種情況是當消費者看到阻塞佇列為空時,開始進入等待,這時生產者一旦往佇列中放入資料,就會通知所有的消費者,喚醒阻塞的消費者執行緒。

  • 另一種情況是如果生產者發現佇列已經滿了,也會被阻塞,而一旦消費者獲取資料之後就相當於佇列空了一個位置,這時消費者就會通知所有正在阻塞的生產者進行生產。

使用 BlockingQueue 實現生產者消費者模式

import java.util.concurrent.ArrayBlockingQueue;

/**
 * 使用阻塞佇列實現一個生產者與消費者模型
 *
 * @author xiandongxie
 */
public class ProducerAndConsumer {

    private static ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(10);

    public static void main(String[] args) throws InterruptedException {
        Producer producer = new Producer();
        Consumer consumer = new Consumer();
        Thread producer1 = new Thread(producer, "producer-1");
        Thread producer2 = new Thread(producer, "producer-2");
        Thread consumer1 = new Thread(consumer, "consumer-2");
        Thread consumer2 = new Thread(consumer, "consumer-2");

        producer1.start();
        producer2.start();
        consumer1.start();
        consumer2.start();

        Thread.sleep(5);
        producer1.interrupt();
        Thread.sleep(5);
        producer2.interrupt();

        Thread.sleep(5);
        consumer1.interrupt();
        consumer2.interrupt();

    }

    static class Producer implements Runnable {
        @Override
        public void run() {
            int count = 0;
            while (true && !Thread.currentThread().isInterrupted()) {
                count++;
                String message = Thread.currentThread().getName() + " message=" + count;
                try {
                    queue.put(message);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }
            }
        }
    }

    static class Consumer implements Runnable {

        @Override
        public void run() {
            while (true && !Thread.currentThread().isInterrupted()) {
                try {
                    String take = queue.take();
                    System.out.println(Thread.currentThread().getName() + ",消費資訊:" + take);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }
            }
        }
    }
}

使用 Condition 實現生產者消費者模式

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 採用 Condition 自定義阻塞佇列實現消費者與生產者
 *
 * @author xiandongxie
 */
public class MyBlockingQueueForCondition<E> {

    private Queue<E> queue;
    private int max = 16;
    private ReentrantLock lock = new ReentrantLock();
    // 沒有空,則消費者可以消費,標記 消費者
    private Condition notEmpty = lock.newCondition();
    // 沒有滿,則生產者可以生產,標記 生產者
    private Condition notFull = lock.newCondition();

    public MyBlockingQueueForCondition(int size) {
        this.max = size;
        queue = new LinkedList();
    }

    public void put(E o) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == max) {
                // 如果滿了,阻塞生產者執行緒,釋放 Lock
                notFull.await();
            }
            queue.add(o);
            // 有資料了,通知等待的消費者,並喚醒
            notEmpty.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public E take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == 0) {
                // 如果為空,阻塞消費者執行緒
                notEmpty.await();
            }
            E item = queue.remove();
            // queue 未滿,喚醒生產者
            notFull.signalAll();
            return item;
        } finally {
            lock.unlock();
        }
    }

}
  • 這裡需要注意,在 take() 方法中使用 while( queue.size() == 0 ) 檢查佇列狀態,而不能用 if( queue.size() == 0 )。
  • 因為生產者消費者往往是多執行緒的,假設有兩個消費者,第一個消費者執行緒獲取資料時,發現佇列為空,便進入等待狀態;
  • 因為第一個執行緒在等待時會釋放 Lock 鎖,所以第二個消費者可以進入並執行 if( queue.size() == 0 ),也發現佇列為空,於是第二個執行緒也進入等待;
  • 而此時,如果生產者生產了一個資料,便會喚醒兩個消費者執行緒,而兩個執行緒中只有一個執行緒可以拿到鎖,並執行 queue.remove 操作,另外一個執行緒因為沒有拿到鎖而卡在被喚醒的地方,而第一個執行緒執行完操作後會在 finally 中通過 unlock 解鎖,而此時第二個執行緒便可以拿到被第一個執行緒釋放的鎖,繼續執行操作,也會去呼叫 queue.remove 操作,然而這個時候佇列已經為空了,所以會丟擲 NoSuchElementException 異常,這不符合邏輯。
  • 而如果用 while 做檢查,當第一個消費者被喚醒得到鎖並移除資料之後,第二個執行緒在執行 remove 前仍會進行 while 檢查,發現此時依然滿足 queue.size() == 0 的條件,就會繼續執行 await 方法,避免了獲取的資料為 null 或丟擲異常的情況。
  • 多執行緒的程式碼大部分都用 while 而不用 if,不管執行緒在哪被切換停止了,while 的話,執行緒上次切換判斷結果對下次切換判斷沒有影響,但是if的話,若執行緒切換前,條件成立過了,但是該執行緒再次拿到 cpu 使用權的時候,其實條件已經不成立了,所以不應該執行。(本質原因:就是原子性問題,CPU 嚴重的原子性是針對 CPU 指令的,而不是針對高階程式語言的語句的)。

使用 wait/notify 實現生產者消費者模式

import java.util.LinkedList;

/**
 * 採用 wait,notify 實現阻塞佇列
 *
 * @author xiandongxie
 */
public class MyBlockingQueue<E> {
    private int maxSize;
    private LinkedList<E> storage;

    public MyBlockingQueue(int maxSize) {
        this.maxSize = maxSize;
        storage = new LinkedList<>();
    }

    public synchronized void put(E e) throws InterruptedException {
        try {
            while (storage.size() == maxSize) {
                // 滿了
                wait();
            }
            storage.add(e);
        } finally {
            notifyAll();
        }

    }

    public synchronized E take() throws InterruptedException {
        try {
            while (storage.size() == 0) {
                // 沒有資料
                wait();
            }
            return storage.remove();
        } finally {
            notifyAll();
        }
    }

}

相關文章