生產者消費者模式是併發、多線程編程中經典的設計模式,生產者和消費者通過分離的執行工作解耦,簡化了開發模式,生產者和消費者可以以不同的速度生產和消費數據。生產者和消費者模式在生活當中隨處可見,它描述的是協調與協作的關係。
比如一個人正在準備食物(生產者),而另一個人正在吃(消費者),
他們共用一張桌子用於放置食物和取走盤食物,生產者準備食物,
如果桌子上已經滿了,生產者就等待,
如果桌子空了的話消費者等待,
這裏桌子就是一個共享的對象。
想象這樣一個情景:
生產者: 在蛋糕店製作蛋糕
消費者: 在蛋糕店賣出蛋糕
盒子: 蛋糕店裏最多存放20個新鮮蛋糕
對於這種生產者消費者模式,Java有三種常用的實現方式:
(1) Synchronized / wait() / notify()方法
(2)Lock / Condition / await() / signal()方法
(3) BlockingQueue阻塞隊列方法
(4) PipedInputStream / PipedOutputStream
以下是前三種方法的演示:
Synchronized / wait() / notify()方法
wait()/ nofity()方法是基類Object的兩個方法,也就意味着所有Java類都會擁有這兩個方法,這樣,我們就可以爲任何對象實現同步機制。
wait():當緩衝區已滿/空時,生產者/消費者線程停止自己的執行,放棄鎖,使自己處於等待狀態,讓其他線程執行。
notify():當生產者/消費者向緩衝區放入/取出一個產品時,向其他等待的線程發出可執行的通知,使自己處於等待狀態。
//盒子模型
class CakeShop{
int num = 0;
//鎖是this
synchronized void increase() throws InterruptedException {
if (num>=20) {//控制蛋糕數不超過20個
this.wait();
}else{
num++;
System.out.println(Thread.currentThread().getName()+"製作了一個蛋糕,蛋糕數:"+num);
Thread.sleep(100);
}
this.notifyAll();
}
//鎖是this
synchronized void decrease() throws InterruptedException {
if (num<=0) {
this.wait();
}else{
num--;
System.out.println(Thread.currentThread().getName()+"出售了一個蛋糕,蛋糕數:"+num);
Thread.sleep(100);
}
this.notifyAll();
}
}
//生產者線程
class Produce implements Runnable {
CakeShop cakeShop;
Produce(CakeShop cakeShop) {
this.cakeShop = cakeShop;
}
@Override
public void run() {
while (true) {
try {
cakeShop.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消費者線程
class Sell implements Runnable{
CakeShop cakeShop;
Sell(CakeShop cakeShop){
this.cakeShop = cakeShop;
}
@Override
public void run() {
while(true){
try {
cakeShop.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//測試類
public class A {
public static void main(String[] args) {
CakeShop cakeShop = new CakeShop();
//10個消費者線程
for(int i = 0;i<10;i++){
new Thread(new Sell(cakeShop)).start();
}
//5個生產者線程
for(int i = 0;i<5;i++){
new Thread(new Produce(cakeShop)).start();
}
}
}
運行結果:
Thread-10製作了一個蛋糕,蛋糕數:1
Thread-9出售了一個蛋糕,蛋糕數:0
Thread-14製作了一個蛋糕,蛋糕數:1
Thread-13製作了一個蛋糕,蛋糕數:2
Thread-13製作了一個蛋糕,蛋糕數:3
Thread-12製作了一個蛋糕,蛋糕數:4
Thread-11製作了一個蛋糕,蛋糕數:5
Thread-11製作了一個蛋糕,蛋糕數:6
Thread-11製作了一個蛋糕,蛋糕數:7
Thread-11製作了一個蛋糕,蛋糕數:8
Thread-12製作了一個蛋糕,蛋糕數:9
Thread-12製作了一個蛋糕,蛋糕數:10
Thread-13製作了一個蛋糕,蛋糕數:11
Thread-13製作了一個蛋糕,蛋糕數:12
Thread-14製作了一個蛋糕,蛋糕數:13
Thread-0出售了一個蛋糕,蛋糕數:12
Thread-0出售了一個蛋糕,蛋糕數:11
Thread-9出售了一個蛋糕,蛋糕數:10
Thread-9出售了一個蛋糕,蛋糕數:9
Thread-9出售了一個蛋糕,蛋糕數:8
Thread-9出售了一個蛋糕,蛋糕數:7
Thread-9出售了一個蛋糕,蛋糕數:6
Thread-9出售了一個蛋糕,蛋糕數:5
Thread-9出售了一個蛋糕,蛋糕數:4
Thread-9出售了一個蛋糕,蛋糕數:3
Thread-9出售了一個蛋糕,蛋糕數:2
Thread-2出售了一個蛋糕,蛋糕數:1
Thread-2出售了一個蛋糕,蛋糕數:0
Thread-10製作了一個蛋糕,蛋糕數:1
Thread-10製作了一個蛋糕,蛋糕數:2
Thread-10製作了一個蛋糕,蛋糕數:3
Thread-10製作了一個蛋糕,蛋糕數:4
Thread-10製作了一個蛋糕,蛋糕數:5
Thread-10製作了一個蛋糕,蛋糕數:6
Thread-10製作了一個蛋糕,蛋糕數:7
Thread-8出售了一個蛋糕,蛋糕數:6
Thread-8出售了一個蛋糕,蛋糕數:5
Thread-8出售了一個蛋糕,蛋糕數:4
Thread-8出售了一個蛋糕,蛋糕數:3
Thread-8出售了一個蛋糕,蛋糕數:2
Thread-8出售了一個蛋糕,蛋糕數:1
Thread-8出售了一個蛋糕,蛋糕數:0
Thread-14製作了一個蛋糕,蛋糕數:1
Thread-13製作了一個蛋糕,蛋糕數:2
Thread-13製作了一個蛋糕,蛋糕數:3
Thread-13製作了一個蛋糕,蛋糕數:4
Thread-13製作了一個蛋糕,蛋糕數:5
......
Lock / Condition / await() / signal()方法
在JDK5.0之後,Java提供了更加健壯的線程處理機制,包括同步、鎖定、線程池等,它們可以實現更細粒度的線程控制。Condition接口的await()和signal()就是其中用來做同步的兩種方法,它們的功能基本上和Object的wait()/ nofity()相同,完全可以取代它們,但是它們和新引入的鎖定機制Lock直接掛鉤,具有更大的靈活性。通過在Lock對象上調用newCondition()方法,將條件變量和一個鎖對象進行綁定,進而控制併發程序訪問競爭資源的安全。下面來看代碼:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class CakeShop{
int num = 0;
Lock lock = new ReentrantLock();
Condition fullCondition = lock.newCondition();//蛋糕過多條件
Condition emptyCondition = lock.newCondition();//沒有蛋糕條件
void increase() throws InterruptedException {
//上鎖
lock.lock();
//控制蛋糕數不超過20個
if (num==20) {
fullCondition.await();
}else{
num++;
System.out.println(Thread.currentThread().getName()+"製作了一個蛋糕,蛋糕數:"+num);
Thread.sleep(100);
}
//喚醒阻塞的所有生產者消費者
fullCondition.signalAll();
emptyCondition.signalAll();
//釋放鎖
lock.unlock();
}
void decrease() throws InterruptedException {
//上鎖
lock.lock();
if (num<1) {
emptyCondition.await();
}else{
num--;
System.out.println(Thread.currentThread().getName()+"吃了一個蛋糕,蛋糕數:"+num);
Thread.sleep(100);
}
//喚醒阻塞的所有生產者消費者
fullCondition.signalAll();
emptyCondition.signalAll();
//釋放鎖
lock.unlock();
}
}
//生產者線程
class Produce implements Runnable {
CakeShop cakeShop;
Produce(CakeShop cakeShop) {
this.cakeShop = cakeShop;
}
@Override
public void run() {
while (true) {
try {
cakeShop.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消費者線程
class Sell implements Runnable{
CakeShop cakeShop;
Sell(CakeShop cakeShop){
this.cakeShop = cakeShop;
}
@Override
public void run() {
while(true){
try {
cakeShop.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//測試類
public class A {
public static void main(String[] args) {
CakeShop cakeShop = new CakeShop();
//10個消費者線程
for(int i = 0;i<10;i++){
new Thread(new Sell(cakeShop)).start();
}
//5個生產者線程
for(int i = 0;i<5;i++){
new Thread(new Produce(cakeShop)).start();
}
}
}
運行結果:
Thread-10製作了一個蛋糕,蛋糕數:1
Thread-10製作了一個蛋糕,蛋糕數:2
Thread-11製作了一個蛋糕,蛋糕數:3
Thread-11製作了一個蛋糕,蛋糕數:4
Thread-12製作了一個蛋糕,蛋糕數:5
Thread-12製作了一個蛋糕,蛋糕數:6
Thread-13製作了一個蛋糕,蛋糕數:7
Thread-13製作了一個蛋糕,蛋糕數:8
Thread-13製作了一個蛋糕,蛋糕數:9
Thread-13製作了一個蛋糕,蛋糕數:10
Thread-13製作了一個蛋糕,蛋糕數:11
Thread-13製作了一個蛋糕,蛋糕數:12
Thread-13製作了一個蛋糕,蛋糕數:13
Thread-13製作了一個蛋糕,蛋糕數:14
Thread-13製作了一個蛋糕,蛋糕數:15
Thread-14製作了一個蛋糕,蛋糕數:16
Thread-14製作了一個蛋糕,蛋糕數:17
Thread-14製作了一個蛋糕,蛋糕數:18
Thread-14製作了一個蛋糕,蛋糕數:19
Thread-14製作了一個蛋糕,蛋糕數:20
Thread-0吃了一個蛋糕,蛋糕數:19
Thread-0吃了一個蛋糕,蛋糕數:18
Thread-0吃了一個蛋糕,蛋糕數:17
Thread-0吃了一個蛋糕,蛋糕數:16
Thread-0吃了一個蛋糕,蛋糕數:15
Thread-0吃了一個蛋糕,蛋糕數:14
Thread-0吃了一個蛋糕,蛋糕數:13
Thread-0吃了一個蛋糕,蛋糕數:12
Thread-0吃了一個蛋糕,蛋糕數:11
Thread-0吃了一個蛋糕,蛋糕數:10
Thread-0吃了一個蛋糕,蛋糕數:9
Thread-0吃了一個蛋糕,蛋糕數:8
Thread-0吃了一個蛋糕,蛋糕數:7
Thread-0吃了一個蛋糕,蛋糕數:6
Thread-0吃了一個蛋糕,蛋糕數:5
Thread-0吃了一個蛋糕,蛋糕數:4
Thread-0吃了一個蛋糕,蛋糕數:3
Thread-0吃了一個蛋糕,蛋糕數:2
Thread-0吃了一個蛋糕,蛋糕數:1
Thread-0吃了一個蛋糕,蛋糕數:0
Thread-10製作了一個蛋糕,蛋糕數:1
Thread-10製作了一個蛋糕,蛋糕數:2
Thread-10製作了一個蛋糕,蛋糕數:3
Thread-11製作了一個蛋糕,蛋糕數:4
......
BlockingQueue阻塞隊列方法
JDK 1.5 以後新增的 java.util.concurrent包新增了 BlockingDeque/ BlockingQueue接口。並提供瞭如下幾種阻塞隊列實現:
java.util.concurrent.ArrayBlockingDeque
java.util.concurrent.LinkedBlockingDeque
java.util.concurrent.SynchronousDeque
java.util.concurrent.PriorityBlockingDeque
實現生產者-消費者模型使用 ArrayBlockingDeque或者 LinkedBlockingDeque即可。
我們這裏使用LinkedBlockingDeque,它是一個已經在內部實現了同步的隊列,實現方式採用的是我們第2種await()/ signal()方法。它可以在生成對象時指定容量大小。它用於阻塞操作的是put()和take()方法。
put()方法:類似於我們上面的生產者線程,容量達到最大時,自動阻塞。
take()方法:類似於我們上面的消費者線程,容量爲0時,自動阻塞。
我們可以跟進源碼看一下LinkedBlockingDeque類的put()方法和take()實現:
先看一下類中的lock和condition:
/** Main lock guarding all access */
final ReentrantLock lock = new ReentrantLock();
/** Condition for waiting takes */
private final Condition notEmpty = lock.newCondition();
/** Condition for waiting puts */
private final Condition notFull = lock.newCondition();
put()方法實現:
public void put(E e) throws InterruptedException {
putLast(e);
}
public void putLast(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
Node<E> node = new Node<E>(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
while (!linkLast(node))
notFull.await();
} finally {
lock.unlock();
}
}
take()方法實現:
public E take() throws InterruptedException {
return takeFirst();
}
public E takeFirst() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E x;
while ( (x = unlinkFirst()) == null)
notEmpty.await();
return x;
} finally {
lock.unlock();
}
}
可見阻塞隊列底層也是使用lock,condition來實現的。
接下來回到正題,看我們的蛋糕店怎麼使用阻塞隊列:
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
class CakeShop{
//存放蛋糕的隊列,設置最大數量爲20
BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>(20);
CakeShop(){//初始化四個蛋糕
blockingDeque.push(1);
blockingDeque.push(1);
blockingDeque.push(1);
blockingDeque.push(1);
}
void increase() throws InterruptedException {
blockingDeque.put(0);
System.out.println(Thread.currentThread().getName()+"生產了一個蛋糕,蛋糕數:"+blockingDeque.size());
}
void decrease() throws InterruptedException {
blockingDeque.take();
System.out.println(Thread.currentThread().getName()+"吃了一個蛋糕,蛋糕數:"+blockingDeque.size());
}
}
//生產者線程n
class Produce implements Runnable {
CakeShop cakeShop;
Produce(CakeShop cakeShop) {
this.cakeShop = cakeShop;
}
@Override
public void run() {
while (true) {
try {
cakeShop.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消費者線程
class Sell implements Runnable{
CakeShop cakeShop;
Sell(CakeShop cakeShop){
this.cakeShop = cakeShop;
}
@Override
public void run() {
while(true){
try {
cakeShop.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//測試類
public class A {
public static void main(String[] args) {
CakeShop cakeShop = new CakeShop();
//10個消費者線程
for(int i = 0;i<10;i++){
new Thread(new Sell(cakeShop)).start();
}
//5個生產者線程
for(int i = 0;i<5;i++){
new Thread(new Produce(cakeShop)).start();
}
}
}
輸出結果:由於輸出時的線程可能會被搶佔,所以輸出的數據不一定符合預期
Thread-0吃了一個蛋糕,蛋糕數:3
Thread-0吃了一個蛋糕,蛋糕數:2
Thread-0吃了一個蛋糕,蛋糕數:1
Thread-0吃了一個蛋糕,蛋糕數:0
Thread-10生產了一個蛋糕,蛋糕數:1
Thread-10生產了一個蛋糕,蛋糕數:1
Thread-10生產了一個蛋糕,蛋糕數:2
Thread-10生產了一個蛋糕,蛋糕數:3
Thread-10生產了一個蛋糕,蛋糕數:3
Thread-10生產了一個蛋糕,蛋糕數:4
Thread-10生產了一個蛋糕,蛋糕數:5
Thread-10生產了一個蛋糕,蛋糕數:6
Thread-10生產了一個蛋糕,蛋糕數:5
Thread-10生產了一個蛋糕,蛋糕數:6
Thread-10生產了一個蛋糕,蛋糕數:7
Thread-10生產了一個蛋糕,蛋糕數:8
Thread-0吃了一個蛋糕,蛋糕數:0
Thread-0吃了一個蛋糕,蛋糕數:7
Thread-10生產了一個蛋糕,蛋糕數:8
Thread-7吃了一個蛋糕,蛋糕數:4
Thread-7吃了一個蛋糕,蛋糕數:3
Thread-7吃了一個蛋糕,蛋糕數:2
Thread-7吃了一個蛋糕,蛋糕數:1
Thread-7吃了一個蛋糕,蛋糕數:0
Thread-4吃了一個蛋糕,蛋糕數:7
Thread-10生產了一個蛋糕,蛋糕數:1
Thread-10生產了一個蛋糕,蛋糕數:2
Thread-10生產了一個蛋糕,蛋糕數:3
Thread-10生產了一個蛋糕,蛋糕數:4
Thread-10生產了一個蛋糕,蛋糕數:4
Thread-10生產了一個蛋糕,蛋糕數:5
Thread-10生產了一個蛋糕,蛋糕數:6
Thread-10生產了一個蛋糕,蛋糕數:7
Thread-10生產了一個蛋糕,蛋糕數:8
Thread-10生產了一個蛋糕,蛋糕數:9
Thread-3吃了一個蛋糕,蛋糕數:4
Thread-3吃了一個蛋糕,蛋糕數:6
Thread-3吃了一個蛋糕,蛋糕數:5
Thread-2吃了一個蛋糕,蛋糕數:5
......