筆者也建立的自己的公衆號啦,平時會分享一些編程知識,歡迎各位大佬支持~
掃碼或微信搜索北風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));
}
}