文章目錄
生產者-消費者模式的多種實現
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
可以利用put、take這一對方法實現生產-消費者模式。
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
- 嘗試自定義實現