生產者-消費者模式的多種實現

生產者-消費者模式的多種實現

1. 生產者-消費者模式介紹

在這裏插入圖片描述
生產者-消費者模式是一個比較經典的問題。

該模式有這麼一些特點:

  • 角色
    • 生產者:負責往緩衝隊列放數據
    • 緩衝隊列:存放數據
    • 消費者:從緩衝隊列取出數據
  • 行爲限制
    • 對於生產者:隊列滿,則阻塞等待(如果佔有鎖,則釋放鎖資源)、否則可以生產數據到隊列
    • 對於消費者:隊列空,則阻塞等待(如果佔有鎖,則釋放鎖資源)、否則從隊列取出數據

2. 考覈技術點

基於特點考慮,考覈多線程相關的知識、線程間通信的知識。

回顧:線程間通信的知識點。

2.1 線程間通信的知識點

關於線程間通信的知識點,以下列出部分和生產者-消費者模式相關的一些。

2.1.1 wait-notify

  • 基於線程的wait、notify(notifyAll)方法實現

二者都是Object類的方法,使用這套方法時必須獲得同步鎖synchronized。

  • wait() 方法:暫停當前線程,並且釋放鎖資源;除非被調用notify(notifyAll)方法喚醒,則有機會繼續往下執行。
  • notify(notifyAll)方法:喚醒需要對象鎖資源的其他線程;但是需要當前線程後面的代碼執行完畢後,其他線程纔開始被調度執行。

2.1.2 JDK的阻塞隊列BlockingQueue

接口java.util.concurrent.BlockingQueue天然具有阻塞、線程安全的特性,所以可以直接使用其實現類實現生產-消費者模式。

拋異常 返回指定值 阻塞 超時
插入 add(e):成功true;失敗異常 offer(e):滿了則返回false put(e):滿了會等待阻塞 off(e, time, unit):設置超時
移除 remove():不存在會空指針異常 poll():空了返回null take():空了會等待阻塞 poll(time, unit):設置超時
判斷 element():對列空拋異常 peek()

其常見實現類有這麼一些:

  • ArrayBlockingQueue

  • LinkedBlockingQueue

  • PriorityBlockingQueue

  • SynchronousQueue

可以利用puttake這一對方法實現生產-消費者模式。

2.1.2 條件信號

在jdk併發包裏面還提供了一個條件接口java.util.concurrent.locks.Condition——條件信號類。

可以理解爲這是一個鎖的條件:通過一個鎖的多個條件可以共享狀態信息。所以對於同一個鎖對象可以創建多個條件。

使用其實例時,建議使用 new Condition() 方法。

不同於synchronized是JVM底層的實現,而Lock是java語言級別的實現控制對象鎖資源。

我們可以使用java.util.concurrent.locks.Lock替換synchronized方法和語句的使用, Condition取代了對象監視器方法的使用。

ReentrantLock lock = new ReentrantLock(); // 鎖
Condition notEmpty = lock.newCondition();  // 條件一
Condition notFull = lock.newCondition();  // 條件二

ReentrantLock

  • 可重入鎖,可以通過代碼手動獲得、釋放鎖資源。
  • API
    • lock():獲得鎖
    • lockInterruptibly():獲得鎖,可以被中斷
    • unlock():釋放鎖
    • newCondition():創建條件對象

Condition

  • 代表一個條件,可以等待、發送通知信號
  • API
    • await():鎖會自動釋放,當前線程暫停,直到該條件發出signal、signalAll信號
    • signal():喚醒一個等待的線程——該線程還需要再次獲得鎖才能執行。
    • signalAll():喚醒所有線程——線程還需要再次獲得鎖才能執行。

使用形式一般如下(Condition的javadoc提供的一個示例代碼),裏面的註釋給出了一個可能按以下順序執行的場景

final Lock lock = new ReentrantLock();
final Condition notFull  = lock.newCondition(); 
final Condition notEmpty = lock.newCondition(); 

final Object[] items = new Object[100];   // 作爲緩衝隊列
int putptr, takeptr, count;

// 生產
public void put(Object x) throws InterruptedException {
    lock.lock();  // 0. 獲得鎖
    try {
        while (count == items.length)   // 1.隊列已滿  
            notFull.await();   // 2.則notFull條件等待--當前線程暫停,會釋放鎖 ; // 7. 重新獲得鎖,隊列不滿則退出循環
        items[putptr] = x;     // 8. 生產數據
        if (++putptr == items.length) putptr = 0;
        ++count;
        notEmpty.signal();    // 9. 喚醒其他線程--notEmpty條件等待的線程
    } finally {
        lock.unlock();        // 10. 釋放鎖,被喚醒的其他線程開始有機會重新獲得鎖了
    }
}

// 消費
public Object take() throws InterruptedException {
    lock.lock();   // 3.獲得鎖
    try {
        while (count == 0)
            notEmpty.await();  // 隊列空,則notEmpty等待,釋放鎖;  // 重新獲得鎖之後,如果有數據則退出循環;還是沒有數據則繼續等待釋放鎖
        Object x = items[takeptr];   // 4. 消費一條數據
        if (++takeptr == items.length) takeptr = 0;
        --count;
        notFull.signal();    // 5. notFull條件喚醒其他線程--此時notFull等待的線程
        return x;    
    } finally {
        lock.unlock();    // 6.釋放鎖;被喚醒的其他線程開始有機會重新獲得鎖了
    }
}

3. 多種實現方式

我們可以利用前面的線程間通信的方式實現生產者-消費者模式。

3.1 JDK自帶的BlockingQueue實現

直接使用阻塞隊列BlockingQueue自帶的put、take方法實現。

  • 生產者
/**
 * 生產者-消費者實現模式一——使用內置的阻塞隊列
 * BlockingQueue的put、take天然支持阻塞等待、線程安全
 */
public class _01_Producer implements Runnable{

    private BlockingQueue<Object> blockingQueue;

    public _01_Producer(BlockingQueue<Object> blockingQueue) {
        this.blockingQueue = blockingQueue;
    }

    @Override
    public void run() {
        for(int i=1; i<=1000; i++){
            try {
                blockingQueue.put(Thread.currentThread().getName()+"-" + i);
                System.out.println("生產者線程[" + Thread.currentThread().getName()
                        + "]生產了" + i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 消費者
public class _01_Consumer implements Runnable {

    private BlockingQueue blockingQueue;

    public _01_Consumer(BlockingQueue blockingQueue) {
        this.blockingQueue = blockingQueue;
    }

    @Override
    public void run() {
        for(int i=1; i<=1000; i++){
            try {
                System.out.println("消費者[" + Thread.currentThread().getName()
                        + "]消費了:" + blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 測試
public class _01_ProducerConsumerTest {
    public static void main(String[] args) {
        BlockingQueue blockingQueue = new ArrayBlockingQueue<>(100);
        new Thread(new _01_Producer(blockingQueue)).start();
        new Thread(new _01_Consumer(blockingQueue)).start();
        new Thread(new _01_Producer(blockingQueue)).start();
        new Thread(new _01_Producer(blockingQueue)).start();
        new Thread(new _01_Consumer(blockingQueue)).start();
        new Thread(new _01_Producer(blockingQueue)).start();
    }
}

消費者[Thread-1]消費了:Thread-0-434
消費者[Thread-1]消費了:Thread-0-435
生產者線程[Thread-2]生產了427
生產者線程[Thread-3]生產了565
消費者[Thread-1]消費了:Thread-0-436
消費者[Thread-1]消費了:Thread-0-437
生產者線程[Thread-0]生產了469
生產者線程[Thread-5]生產了578
消費者[Thread-1]消費了:Thread-0-438
消費者[Thread-1]消費了:Thread-0-439
生產者線程[Thread-2]生產了428

3.2 synchronized+wait+notifyAll實現

使用同步字synchronized結合wait、notify(notifyAll)實現。

  • 提供生產、消費方法
public class _02_ProducerConsumer<E> {
    private Queue<E> queue;   // 緩衝隊列
    private int capacity;     // 容量

    public _02_ProducerConsumer() {
        this(16);
    }

    public _02_ProducerConsumer(int capacity) {
        queue = new LinkedList<>();
        this.capacity = capacity;
    }


    public synchronized void pro(E e) throws InterruptedException {
        // 滿了則等待
        while (queue.size() == capacity) {
            // 滿了則等待;釋放鎖;等待其他線程notify、notifyAll才喚醒重新獲得鎖
            this.wait();
        }
        if( queue.size() == 0 ){
            // 喚醒其他線程,在當前線程釋放鎖之前其他線程只是就緒並不會立馬執行
            this.notifyAll();
        }
        queue.add(e);
    }

    /**
     * 消費
     * @return
     * @throws InterruptedException
     */
    public synchronized E con() throws InterruptedException {
        // 空則等待
        while (queue.size() == 0) {
            this.wait();
        }
        if( queue.size() == capacity ){
            this.notifyAll();
        }
        return queue.remove();
    }
}

  • 測試
public static void main(String[] args) {

        _02_ProducerConsumer<Integer> producerConsumer = new _02_ProducerConsumer();

        ExecutorService es = Executors.newFixedThreadPool(2);


        es.execute(()->{
            while (true) {
                try {
                    int nextInt = new Random().nextInt(1000);
                    System.out.println("生產者[" + Thread.currentThread().getName()
                            + "]生產:" + nextInt);
                    producerConsumer.pro(nextInt);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });


        es.execute(()->{
            while (true) {
                try {
                    Integer integer = producerConsumer.con();
                    System.out.println("消費者["+Thread.currentThread().getName()
                            +"]獲得:" + integer);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

    }

生產者[pool-1-thread-1]生產:418
生產者[pool-1-thread-1]生產:222
消費者[pool-1-thread-2]獲得:418
消費者[pool-1-thread-2]獲得:222
生產者[pool-1-thread-1]生產:278
生產者[pool-1-thread-1]生產:59
消費者[pool-1-thread-2]獲得:278
消費者[pool-1-thread-2]獲得:59
生產者[pool-1-thread-1]生產:217

3.3 ReentrantLock+Condition實現

使用ReentrantLock+Condition自定義一個阻塞隊列。

  • 阻塞隊列實現
public class _03_MyBlockingQueue<E> {
    private Queue<E> queue;
    private int capacity;

    final ReentrantLock lock = new ReentrantLock();
    final Condition notEmpty = lock.newCondition();
    final Condition notFull = lock.newCondition();

    public _03_MyBlockingQueue() {
        this(16);
    }

    public _03_MyBlockingQueue(int capacity) {
        this.queue = new LinkedList();
        this.capacity = capacity;
    }


    // 生產
    public void pro(E e) throws InterruptedException {
        // 可中斷鎖
        lock.lockInterruptibly();

        try {
            if (queue.size() == capacity) {
                // 鎖會自動釋放,線程暫停直到該條件(notFull)發出signal、signalAll
                notFull.await();
            }
            if (queue.size() == 0) {
                notEmpty.signal();
            }
            queue.add(e);
        } finally {
            lock.unlock();
        }
    }

    // 消費
    public E con() throws InterruptedException {
        lock.lockInterruptibly();

        try {
            if (queue.size() == 0) {
                notEmpty.await();
            }

            if (queue.size() == capacity) {
                notFull.signal();
            }
            return queue.remove();
        } finally {
            lock.unlock();
        }
    }
}
  • 測試
public static void main(String[] args)  {
        _03_MyBlockingQueue blockingQueue = new _03_MyBlockingQueue<>();
        new Thread(()->{
            while (true) {
                int nextInt = new Random().nextInt(1000);
                try {
                    blockingQueue.pro(nextInt);
                    System.out.println("生產:" + nextInt);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(()->{
            while (true) {
                try {
                    Object con = blockingQueue.con();
                    System.out.println("消費:" + con);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

生產:620
生產:728
消費:79
消費:307
生產:970
生產:759
消費:191
消費:624
消費:485
消費:982

總結

  • 先理清生產者-消費者模式的概念、特徵
  • 回顧JDK的自帶API
  • 嘗試自定義實現
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章