The producer-consumer model is a classic multi-threaded design model, which provides a good solution for multi-threaded collaboration. The producer-consumer model is not one of the 23 OO (Object Oriented) design patterns of GOF, but a non-OO design pattern.
In the producer-consumer mode, there are usually two types of threads: the producer thread responsible for submitting user requests and the consumer thread responsible for specifically processing producer-submitted requests (user requests).
In the producer-consumer model, the producer and the consumer do not communicate directly, but first submit the task to the shared buffer, and then the consumer gets the task from the shared buffer and processes it.
The core of the producer-consumer model is the shared memory buffer. As a bridge between producer and consumer, memory buffer avoids direct communication between producer and consumer, thus decoupling producer and consumer. It is necessary to use memory buffer here. With memory buffer, the producer does not have to wait for the consumer to receive the task, but directly puts the task into the buffer to start the next task of "production". Consumers are similar. This enables producers and consumers to be independent and concurrent, alleviates the time lag in execution speed between producers and consumers, and supports the uneven situation of busyness.
Realization
Here we first define the data model PCDada for interaction between producers and consumers, which is used in all subsequent implementations.
/**
* Created by zhangcs on 17-4-16.
*/
public final class PCData {
private int intData;
public PCData( int intData){
this.intData = intData;
}
public int getIntData() {
return intData;
}
@Override
public String toString(){
return "data:" + intData;
}
}
1. Using synchronized lock and wait(), notify() method to realize
The implementation of wait() and notify() methods is relatively simple. Firstly, buffer is defined. List is used for convenience; secondly, buffer size is defined. When buffer is full or buffer is empty, producer and consumer are blocked respectively.
/**
* Created by zhangcs on 17-4-16.
* The producer inserts the task into the buffer
*/
public class Producer implements Runnable {
// Buffer space maximum capacity
private final int MAX_SIZE = 10;
// Counter
// Because the self-incremental operation of int is not thread-safe, Atomic Integer is used here.
private static AtomicInteger counter = new AtomicInteger();
private List<PCData> list;
public Producer( List<PCData> list){
this.list = list;
}
@Override
public void run() {
synchronized (list){
while ( list.size() > MAX_SIZE){
System.out.println( "Buffer full, producer blocked");
try{
// Production is blocked due to unsatisfactory conditions
list.wait();
} catch( InterruptedException e){
e.printStackTrace();
}
}
// Create tasks and add buffers
PCData data = new PCData( counter.incrementAndGet());
list.add( data);
System.out.println( data + " Join the queue");
list.notifyAll();
}
}
public int getMAX_SIZE(){
return MAX_SIZE;
}
}
/**
* Created by zhangcs on 17-4-16.
* Consumers get tasks from buffers and process them
*/
public class Consumer implements Runnable {
private List<PCData> list;
public Consumer( List<PCData> list){
this.list = list;
}
@Override
public void run() {
synchronized (list){
while( true){
while ( list.size() <= 0){
System.out.println("Buffer no task, consumer blocking");
try{
// Consumption congestion due to unsatisfactory conditions
list.wait();
} catch( InterruptedException e){
e.printStackTrace();
}
}
// consumption
PCData data = list.get( 0);
list.remove( 0);
System.out.println( "Consumer Consumption" + data );
list.notifyAll();
}
}
}
}
When using, pass the same buffer LinkedList to producer and consumer respectively.
@Test
public void test() throws InterruptedException {
// Buffer
List<PCData> list = new LinkedList<>();
// Create multiple producers
Producer producer1 = new Producer( list);
Producer producer2 = new Producer( list);
Producer producer3 = new Producer( list);
Producer producer4 = new Producer( list);
// Creating Consumers
Consumer consumer = new Consumer( list);
// Create thread pools and execute producers and consumers
ExecutorService service = Executors.newCachedThreadPool();
service.execute( producer1);
service.execute( producer2);
service.execute( producer3);
service.execute( producer4);
service.execute( consumer);
Thread.sleep( 10 * 1000);
service.shutdown();
}
2. Using Lock lock and await() and signal() methods
Using await() and signal() methods can achieve the effect that producers and consumers can share the lock object or use the lock object separately, which is similar to the above method. Here's an example of shared locks
/**
* Created by zhangcs on 17-4-17.
* The producer inserts the task into the buffer
*/
public class Producer implements Runnable {
// Buffer size
private final int MAX_VALUE = 10;
// Carrier/Buffer
private List<PCData> list;
// Counter
private static AtomicInteger counter = new AtomicInteger();
// lock
private final Lock lock;
// Conditional variable with full buffer
private final Condition full;
// Conditional variable with empty buffer
private final Condition empty;
// Locks are introduced here, so that producers and consumers share the same lock.
public Producer( List<PCData> list, Lock lock, Condition full, Condition empty){
this.list = list;
this.lock = lock;
this.full = full;
this.empty = empty;
}
@Override
public void run() {
// Lock up
lock.lock();
while( list.size() >= MAX_VALUE){
System.out.println( "Buffer full, producer blocked");
try{
// Buffer full, blocking threads
full.await();
}catch( InterruptedException e){
e.printStackTrace();
}
}
// production
PCData data = new PCData( counter.incrementAndGet());
list.add( data);
System.out.println( data + " Join the queue");
full.signalAll();
empty.signalAll();
// Release lock
lock.unlock();
}
}
/**
* Created by zhangcs on 17-4-17.
* Consumers get tasks from buffers and process them
*/
public class Consumer implements Runnable {
private List<PCData> list;
// lock
private final Lock lock;
// Conditional variable with full buffer
private final Condition full;
// Conditional variable with empty buffer
private final Condition empty;
// Locks are introduced here, so that producers and consumers share the same lock.
public Consumer( List<PCData> list, Lock lock, Condition full, Condition empty){
this.list = list;
this.lock = lock;
this.full = full;
this.empty = empty;
}
@Override
public void run() {
while( true){
// Lock up
lock.lock();
while( list.size() <= 0){
System.out.println("Buffer no task, consumer blocking");
try{
// Consumption congestion due to unsatisfactory conditions
empty.await();
} catch( InterruptedException e){
e.printStackTrace();
}
}
// consumption
PCData data = list.get( 0);
list.remove( 0);
System.out.println( "Consumer Consumption" + data );
full.signalAll();
empty.signalAll();
// Release lock
lock.unlock();
}
}
}
Common buffers and locks are passed in when used
@Test
public void test() throws InterruptedException {
// Creating Buffers
List<PCData> list = new LinkedList<>();
// Create locks
Lock lock = new ReentrantLock();
Condition full = lock.newCondition();
Condition empty = lock.newCondition();
// Creating Producers
Producer producer1 = new Producer( list, lock, full, empty);
Producer producer2 = new Producer( list, lock, full, empty);
Producer producer3 = new Producer( list, lock, full, empty);
// Creating Consumers
Consumer consumer = new Consumer( list, lock, full, empty);
// Create thread pools and execute producer and consumer threads
ExecutorService service = Executors.newCachedThreadPool();
service.execute( producer1);
service.execute( producer2);
service.execute( producer3);
service.execute( consumer);
Thread.sleep( 10 * 1000);
service.shutdown();
}
3. BlockingQueue implementation using blocking queue
There are five common BlockingQueue implementation classes, which can be selected according to the requirements.
Implementation class | Explain |
---|---|
ArrayBlockingQueue | Based on arrays, producers and consumers share the same lock object for data acquisition, which means that they cannot run in real parallel. At the time of creation, it can control whether the internal lock of the object is fair or not, and default to unfair lock. |
LinkedBlockingQuque | Based on linked list, when the internal data cache list reaches the maximum cache capacity, it will block. Producers and consumers use different locks to control it. |
PriorityBlockingQueue | Priority-based blocking queues block consumers only when there is no consumable data. Note: Producers cannot produce data faster than consumers, otherwise time will run out of all available heap memory space. |
DelayQueue | Elements can be retrieved from the queue only when the specified delay time has arrived. There is no size limit, so when inserting data into a queue, it will never block, only when the data is retrieved. |
SynchronousQueue | There is no buffer waiting queue, no data buffer space inside (its isEmpty() method always returns true), so data elements may exist only when consumers obtain them. Similar to the one-hand delivery scenario in real life, data is transmitted directly between the paired producers and consumers, and does not buffer the data into the data queue. SynchronousQueue is generally used for thread pools, such as Executors.newCachedThreadPool() using SynchronousQueue. |
For the specific use of each BlockingQueue implementation class, refer to the API yourself. Here's a direct example
/**
* Created by zhangcs on 17-4-16.
* The producer inserts the task into the buffer
*/
public class Producer implements Runnable {
private volatile boolean isRunning = true;
private BlockingQueue<PCData> queue;
// Because the incremental operation of int is not atomic, Atomic Integer is used here for counting.
private static AtomicInteger integer = new AtomicInteger();
public Producer( BlockingQueue<PCData> queues){
this.queue = queues;
}
@Override
public void run() {
PCData data = null;
try{
while ( isRunning) {
Thread.sleep( 500);
data = new PCData( integer.incrementAndGet());
System.out.println( data + " Join the queue");
if( !queue.offer( data, 2, TimeUnit.SECONDS)){
System.out.println( data + "Failure to join");
}
}
}catch( InterruptedException ie){
ie.printStackTrace();
Thread.currentThread().interrupt();
}
}
public void stop(){
isRunning = false;
}
}
/**
* Created by zhangcs on 17-4-16.
* Consumers get tasks from buffers and process them
*/
public class Consumer implements Runnable {
private BlockingQueue<PCData> queue;
public Consumer( BlockingQueue<PCData> queue){
this.queue = queue;
}
@Override
public void run() {
try{
while (true){
PCData data = queue.take();
if ( data != null){
System.out.println( "Consumer Consumption" + data);
// Waiting for 2000 ms to simulate the difference in execution speed between producers and consumers
Thread.sleep( 2 * 1000);
}
}
}catch( InterruptedException ie){
ie.printStackTrace();
Thread.currentThread().interrupt();
}
}
}
Create a buffer when used and pass it to both producers and consumers
public static void main( String[] args) throws InterruptedException {
// Buffer Establishment
BlockingQueue<PCData> queue = new LinkedBlockingQueue<>(10);
Producer producer1 = new Producer( queue); // Establishing Producers
Producer producer2 = new Producer( queue);
Consumer consumer1 = new Consumer( queue); // Establishing Consumers
// Running producers and consumers with thread pools
ExecutorService service = Executors.newCachedThreadPool();
service.execute( producer1);
service.execute( producer2);
service.execute( consumer1);
Thread.sleep( 20 * 1000);
// Stop Producers
producer1.stop();
producer2.stop();
Thread.sleep( 13000);
service.shutdown();
}
4. Pipeline method
The above methods are essentially the same, they are based on thread concurrency and then use locks to synchronize. Unlike pipelines, pipelines are process-based concurrency. The use of pipelines does not need to worry about thread safety, memory allocation and other issues, which is conducive to reducing development costs and debugging costs. Pipeline method has obvious advantages, but its disadvantages are also obvious. It is only suitable for one-to-one situation between producers and consumers, and can not communicate across machines. It is not practical in actual scenarios.