Java多線程之生產者消費者模式

生產者消費者模式是併發、多線程編程中經典的設計模式,生產者和消費者通過分離的執行工作解耦,簡化了開發模式,生產者和消費者可以以不同的速度生產和消費數據。生產者和消費者模式在生活當中隨處可見,它描述的是協調與協作的關係。

比如一個人正在準備食物(生產者),而另一個人正在吃(消費者),
他們共用一張桌子用於放置食物和取走盤食物,生產者準備食物,
如果桌子上已經滿了,生產者就等待,
如果桌子空了的話消費者等待,
這裏桌子就是一個共享的對象。

想象這樣一個情景:

生產者: 在蛋糕店製作蛋糕
消費者: 在蛋糕店賣出蛋糕
盒子: 蛋糕店裏最多存放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
......
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章