深入淺出生產者-消費者模式

筆者也建立的自己的公衆號啦,平時會分享一些編程知識,歡迎各位大佬支持~

掃碼或微信搜索北風IT之路關注

生產者-消費者模式是一個經典的多線程設計模式,它爲多線程間的協作提供了良好的解決方案。也經常有面試官會讓手寫一個生產者消費者,從代碼細節可以看出你對多線程編程的熟練程度,今天我們來詳細看一下如何寫出一個生產者消費者模式,並且逐步對其優化爭取做到高性能。

結構剖析

在生產者-消費者模式中,通常有兩類線程,一類是生產者線程一類是消費者線程。生產者線程負責提交用戶請求,消費者線程則負責處理生產者提交的任務。

最簡單粗暴的做法就是生產者每提交一個任務,消費者就立即處理,直到所有任務處理完。但是這樣直接通信很容易出現性能上的問題,消費者必須等待它的生產者提交到任務才能執行,就不能達到真正的並行。同時生產者和消費者之間存在依賴關係,在設計上耦合度非常高,這是不可取的。那麼最好的做法就是加一箇中間層作爲通信橋樑。

生產者和消費者之間通過共享內存緩存區進行通信。多個生產者線程將任務提交給共享內存緩存區,消費者線程並不直接與生產者線程通信,而在共享內存緩衝區獲取任務,並行地處理。其中內存緩衝區的主要功能是數據再多線程間的共享,同時還可以通過該緩存區,緩解生產者和消費者間的性能差。它是生產者消費者模式的核心組件,既能作爲通信的橋樑,又能避免兩者直接通信,從而將生產者和消費者進行解耦。生產者不需要消費者的存在,消費者也不需要知道生產者的存在。

由於內存緩衝區的存在,允許生產者和消費者在執行速度上有差異,無論哪一方速度超過另一方,緩衝區都可以得到緩解,確保系統正常運行。

生產者-消費者模式UML圖如下:
在這裏插入圖片描述

實測

下面是我用IDEA的工具生成的UML圖

其中Queue的選擇最爲關鍵,一般情況下應該會選擇阻塞式隊列,也就是BlockingQueue,但是它的併發效果並不好,所以我選擇了ConcurrentLinkedQueue,筆者經過實測,生產和消費100萬個數據,使用LinkedBlockingQueue耗時是38秒,並且在處理了防超量生產的情況下仍然產生了超量生產,其原因就是其併發效果不如後者,後者使用的是CAS無鎖實現,而使用ConcurrentLinkedQueue耗時是27秒,無超量生產情況產生。其中ConcurrentLinkedQueue運行時佔用內存最大時爲546MB(jconsole測得)。

代碼實現

Data類

package thread.producerConsumer;

/**
 * @author beifengtz
 * <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
 * <p>location: thread.producerConsumer.javase_learning</p>
 * Created in 19:11 2019/5/19
 */
public class Data {
    private final int data;

    public Data(int data) {
        this.data = data;
    }

    public int getData() {
        return data;
    }

    @Override
    public String toString() {
        return "Data{" +
                "data=" + data +
                '}';
    }
}

生產者Producer

package thread.producerConsumer;

import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author beifengtz
 * <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
 * <p>location: thread.producerConsumer.javase_learning</p>
 * Created in 19:11 2019/5/19
 */
public class Producer implements Runnable {
    //  緩衝隊列
    private ConcurrentLinkedQueue<Data> queue;
    //  每個生產者生產數據的數量
    private int produceNum;

    //  任務計數,同時模擬數據生產
    private static AtomicInteger count = new AtomicInteger(0);
    //  需要生產的總任務數量,出處有耦合性,實際開發時應當處理。
    private static AtomicInteger total = new AtomicInteger(Main.NUMS);

    public Producer(ConcurrentLinkedQueue<Data> queue, int produceNum) {
        this.queue = queue;
        this.produceNum = produceNum;
    }

    @Override
    public void run() {
        Data data = null;
        //  限定生產,防止超量生產
        if (total.get() <= 0) {
            Thread.currentThread().interrupt();
        }
        for (int i = 0; i < produceNum; i++) {
            //  再次檢測,防止超量生產
            if (total.get() > 0) {
                //  構造數據
                data = new Data(count.incrementAndGet());
                //  向緩衝隊列中提交
                if (!queue.offer(data)) {
                    System.out.println("Filed to put data:" + data);
                } else {
                    System.out.println("Successfully produced data:" + data);
                }
                //  需要生產的總量-1
                total.decrementAndGet();
            } else {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }
}

消費者Consumer

package thread.producerConsumer;

import java.text.MessageFormat;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;

/**
 * @author beifengtz
 * <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
 * <p>location: thread.producerConsumer.javase_learning</p>
 * Created in 19:11 2019/5/19
 */
public class Consumer implements Runnable {
    private ConcurrentLinkedQueue<Data> queue;
    private CountDownLatch countDown;

    public Consumer(ConcurrentLinkedQueue<Data> queue, CountDownLatch countDown) {
        this.queue = queue;
        this.countDown = countDown;
    }

    @Override
    public void run() {
        Data data = queue.poll();
        if (data != null) {
            //  計算+1
            int res = data.getData() + 1;
            System.out.println("Data processing: " + MessageFormat.format("{0}+1={1}", data.getData(), res));
            //  記錄該線程任務已結束
            countDown.countDown();
        }
    }
}

Main運行主類

package thread.producerConsumer;

import java.util.concurrent.*;

/**
 * @author beifengtz
 * <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
 * <p>location: thread.producerConsumer.javase_learning</p>
 * Created in 19:10 2019/5/19
 */
public class Main {
    public static final int NUMS = 1000000;

    public static void main(String[] args) {

        //  定義緩衝隊列
        ConcurrentLinkedQueue<Data> queue = new ConcurrentLinkedQueue<>();
        //  創建生產者和消費者線程池,爲防止衝爆內存,這裏限定線程數
        ExecutorService producerEs = Executors.newFixedThreadPool(100);
        ExecutorService consumerEs = Executors.newFixedThreadPool(100);
        //  定義計時器,等待消費者全部消費完
        CountDownLatch countDown = new CountDownLatch(NUMS);

        long start = System.currentTimeMillis();
        long end = System.currentTimeMillis();

        //  向線程池中提交 NUMS 個生產任務
        for (int i = 0; i < NUMS; i++) {
            //  每個生產者以3倍的量生產,模擬生產者和消費者的速度差
            producerEs.execute(new Producer(queue,3));
            consumerEs.execute(new Consumer(queue, countDown));
        }
        try {
            countDown.await();
            end = System.currentTimeMillis();
            producerEs.shutdown();
            consumerEs.shutdown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Total time:" + (end - start));
    }
}

Disruptor

Disruptor是一個高效的無鎖內存隊列,它使用無鎖的方式實現了一個有界環形隊列,非常適合生產者-消費者模式。它相比於BlockingQueue具有更多的特點:

  • 同一個“事件”可以有多個消費者,消費者之間既可以並行處理,也可以相互依賴形成處理的先後次序(形成一個依賴圖);
  • 預分配用於存儲事件內容的內存空間;
  • 針對極高的性能目標而實現的極度優化和無鎖的設計。

這裏簡單引用另一篇文章(https://www.jianshu.com/p/8473bbb556af)中的一部分Disruptor介紹

  • Ring Buffer

如其名,環形的緩衝區。曾經 RingBuffer 是 Disruptor 中的最主要的對象,但從3.0版本開始,其職責被簡化爲僅僅負責對通過 Disruptor 進行交換的數據(事件)進行存儲和更新。在一些更高級的應用場景中,Ring Buffer 可以由用戶的自定義實現來完全替代。

  • Sequence Disruptor

通過順序遞增的序號來編號管理通過其進行交換的數據(事件),對數據(事件)的處理過程總是沿着序號逐個遞增處理。一個 Sequence 用於跟蹤標識某個特定的事件處理者( RingBuffer/Consumer )的處理進度。雖然一個 AtomicLong 也可以用於標識進度,但定義 Sequence 來負責該問題還有另一個目的,那就是防止不同的 Sequence 之間的CPU緩存僞共享(Flase Sharing)問題。
(注:這是 Disruptor 實現高性能的關鍵點之一,網上關於僞共享問題的介紹已經汗牛充棟,在此不再贅述)。

  • Sequencer

Sequencer 是 Disruptor 的真正核心。此接口有兩個實現類 SingleProducerSequencer、MultiProducerSequencer ,它們定義在生產者和消費者之間快速、正確地傳遞數據的併發算法。

  • Sequence Barrier

用於保持對RingBuffer的 main published Sequence 和Consumer依賴的其它Consumer的 Sequence 的引用。 Sequence Barrier 還定義了決定 Consumer 是否還有可處理的事件的邏輯。

  • Wait Strategy

定義 Consumer 如何進行等待下一個事件的策略。 (注:Disruptor 定義了多種不同的策略,針對不同的場景,提供了不一樣的性能表現)

  • Event

在 Disruptor 的語義中,生產者和消費者之間進行交換的數據被稱爲事件(Event)。它不是一個被 Disruptor 定義的特定類型,而是由 Disruptor 的使用者定義並指定。

  • EventProcessor

EventProcessor 持有特定消費者(Consumer)的 Sequence,並提供用於調用事件處理實現的事件循環(Event Loop)。

  • EventHandler

Disruptor 定義的事件處理接口,由用戶實現,用於處理事件,是 Consumer 的真正實現。

  • Producer

即生產者,只是泛指調用 Disruptor 發佈事件的用戶代碼,Disruptor 沒有定義特定接口或類型。

Disruptor代碼實現

筆者簡單地寫了一個用Disruptor實現的程序,結果驚人!同樣是100萬個數據,時間消耗只用了22秒,時間上和前面ConcurrentLinkedQueue差別不大,但有所提升,但是它的內存消耗居然最高時僅使用了60MB!!!在內存上節省了9倍的空間,可以看到這種內存複用的策略很有效果,在實際開發中很有幫助。

提示:Disruptor是第三方包,需要下載jar包導入項目或者使用Maven導入

數據類Data

package thread.producerConsumer.disruptor;

/**
 * @author beifengtz
 * <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
 * <p>location: thread.producerConsumer.disruptor.javase_learning</p>
 * Created in 21:57 2019/5/19
 */
public class Data {
    private long value;

    public long getValue() {
        return value;
    }

    public void setValue(long value) {
        this.value = value;
    }
}

數據生產工廠

package thread.producerConsumer.disruptor;

import com.lmax.disruptor.EventFactory;

/**
 * @author beifengtz
 * <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
 * <p>location: thread.producerConsumer.disruptor.javase_learning</p>
 * Created in 21:56 2019/5/19
 */
public class DataFactory implements EventFactory<Data> {
    @Override
    public Data newInstance() {
        return new Data();
    }
}

生產者Prodocer

package thread.producerConsumer.disruptor;

import com.lmax.disruptor.RingBuffer;

import java.nio.ByteBuffer;

/**
 * @author beifengtz
 * <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
 * <p>location: thread.producerConsumer.disruptor.javase_learning</p>
 * Created in 21:59 2019/5/19
 */
public class Producer {
    private final RingBuffer<Data> ringBuffer;

    public Producer(RingBuffer<Data> ringBuffer) {
        this.ringBuffer = ringBuffer;
    }

    public void pushData(ByteBuffer bb){
        long sequence = ringBuffer.next();
        try{
            Data even = ringBuffer.get(sequence);
            even.setValue(bb.getLong(0));
        }finally {
            ringBuffer.publish(sequence);
        }
    }
}

消費者Consumer

package thread.producerConsumer.disruptor;

import com.lmax.disruptor.WorkHandler;

import java.text.MessageFormat;

/**
 * @author beifengtz
 * <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
 * <p>location: thread.producerConsumer.disruptor.javase_learning</p>
 * Created in 21:51 2019/5/19
 */
public class Consumer implements WorkHandler<Data> {
    @Override
    public void onEvent(Data data) throws Exception {
        long res = data.getValue() + 1;
        System.out.println(MessageFormat.format("Data processing: {0} + 1 = {1}",data.getValue(),res));
    }
}

運行主類Main

package thread.producerConsumer.disruptor;

import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;

import java.nio.ByteBuffer;
import java.util.concurrent.ThreadFactory;

/**
 * @author beifengtz
 * <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
 * <p>location: thread.producerConsumer.disruptor.javase_learning</p>
 * Created in 21:50 2019/5/19
 */
public class Main {
    private static final int NUMS = 10;
    private static final int SUM = 1000000;

    public static void main(String[] args) {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long start = System.currentTimeMillis();
        DataFactory factory = new DataFactory();
        int bufferSize = 1024;
        Disruptor<Data> disruptor = new Disruptor<>(factory, bufferSize, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r);
            }
        });

        //  構造100個消費者
        Consumer[] consumers = new Consumer[NUMS];
        for (int i = 0; i < NUMS; i++) {
            consumers[i] = new Consumer();
        }
        disruptor.handleEventsWithWorkerPool(consumers);
        disruptor.start();

        RingBuffer<Data> ringBuffer = disruptor.getRingBuffer();
        Producer producer = new Producer(ringBuffer);
        ByteBuffer bb = ByteBuffer.allocate(8);
        for (long l = 0; l < SUM; l++) {
            bb.putLong(0, l);
            producer.pushData(bb);
            System.out.println("Successfully produced data: " + l);
        }
        long end = System.currentTimeMillis();
        disruptor.shutdown();
        System.out.println("Total time: " + (end - start));
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章