[Java] Understand the producer and consumer models in one article

The concept of blocking queue

The queue was introduced before, which is a data structure, first in first out FIFO. The blocking queue also satisfies the characteristics of the queue, but there is a special feature:
when entering the queue element, first judge whether the queue is full, if it is full, wait (block), and then insert when there is free space
; Determine whether the queue is empty, wait (block) if it is empty, and take it out when there are elements in the queue.

Example in real life: making dumplings
1. Everyone rolls their own dumpling wrappers and makes their own dumplings. In
this situation, there is a high probability that there will be competition for rolling pins, which is lock competition in a multi-threaded environment.
2. One person is dedicated to rolling the dumpling wrappers, and the others are responsible for making the dumplings.
When there are many dumpling wrappers, the person rolling the dumpling wrappers can rest for a while; when there are no dumpling wrappers, they have to keep rolling
; Those who make dumplings have to keep making dumplings; when there are no dumpling wrappers, they stop and rest;
insert image description here

In the above example, the person who rolls the wrappers can become a producer, and the person who makes dumplings is called a consumer. The place where the dumpling wrappers are placed is a trading place, which can be realized by blocking queues. This is a typical application scenario of blocking queues - " producer consumer model ", which is a very typical development model.

producer consumer pattern

The producer-consumer model uses a container to solve the problem of strong coupling between producers and consumers. Producers and consumers do not communicate directly with each other, but communicate through blocking queues. Therefore, after producing data, producers do not need to wait for consumers to process it, but directly throw it to the blocking queue. Consumers do not ask producers for data, but Take it directly from the blocking queue. In a specific project, a "middleware" such as a message queue is used to realize this function.

message queue

On the basic data structure, some optimizations and implementations for application scenarios have been made, and such frameworks and software are called "middleware" . The message queue is a "middleware", which is essentially a blocking queue. On this basis, the messages put into the blocking queue are tagged. For example, in the case of selling steamed stuffed buns:
insert image description here
if all the buns that have just come out of the pan are red bean paste buns, then you only need to process the news about buying red bean paste buns, and the tags of the messages can realize the function of grouping .

The role of the message queue

1. Decoupling
For example, in the following situation, server A and server B must know the existence of each other, and the parameters must be agreed. If one of them hangs up, the entire process will be affected. In this case, if a certain function needs to be modified, the involved servers may need to modify the code. In this case, the coupling degree is very high, and this system organization method is not recommended.
insert image description here
The decoupling of the three systems through the message queue means that they no longer communicate directly, and achieve the effect of separate maintenance and operation .
insert image description here
Some requirements were put forward when designing the program: such as high cohesion and low coupling . High cohesion is to write codes with strong functions together, which is very convenient to maintain. It's a way of organizing code. Low coupling means not to write the same code everywhere. Generally, the code is encapsulated into a method in an abstract way, and it can be called when it is used. Good code organization can effectively reduce maintenance costs.

2. Cut peaks and fill
valleys Peaks and valleys refer to the intensity of news.
For example, during Double Eleven, the traffic will increase sharply. In this link, any problem with any node will affect the entire business process.
insert image description here
At this time, you may think that you can use multiple groups of links, and different users can access through different links (load balancing), which can solve the problem caused by the sudden increase in traffic. This problem can indeed be solved, but what needs to be considered is that the traffic will not always be at the peak, and the traffic will be normal most of the time. At this time, other links deployed will not be used, which will undoubtedly increase the cost several times. .

Suppose the bank can only process 200 orders per second, and the logistics company can only process 100 orders per second. When calling the third-party interface, if the number of calls reaches the upper limit, it will be blocked for a while. At this time, the message queue is used. When the traffic surges, the message queue is used to buffer (peak clipping) , and when the traffic decreases, the messages stored in the message queue are consumed little by little (filling the valley) . Finally, let the system and hardware configuration reach a balance.
insert image description here

3. Asynchronous
Synchronization means that the requester must wait for the response from the other party.
Asynchronous means that after sending a request, you can do other things by yourself, and when there is a response, you will receive a notification to process the response .

Blocking Queue in JDK

JDK provides a variety of different blocking queues, and you can choose different blocking queue implementations according to different business scenarios.

public class Demo01_BlockingQueue {
    
    
    public static void main(String[] args) {
    
    
        //定义阻塞队列
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(3);
        BlockingQueue<Integer> queue1 = new ArrayBlockingQueue<>(3);
        BlockingQueue<Integer> queue2 = new PriorityBlockingQueue<>(3);
    }
}

1. Adding elements
The initial capacity can be given when defining the blocking queue. In the example below, since the current blocking capacity is 3, blocking occurs when the fourth element is inserted.

public class Demo01_BlockingQueue {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        //定义一个阻塞队列
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(3);
        //往队列中写入元素
        queue.put(1);
        queue.put(2);
        queue.put(3);
        System.out.println("已经插入了三个元素");
        queue.put(4);
        System.out.println("已经插入了第四个元素");
    }
}

insert image description here
2. Take out elements

Obtaining elements in the blocking queue does not use the poll() method, but uses the take() method, which will produce a blocking effect .
insert image description here

public class Demo01_BlockingQueue {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        //定义一个阻塞队列
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(3);
        //往队列中写入元素
        queue.put(1);
        queue.put(2);
        queue.put(3);
        System.out.println("已经插入了三个元素");
        System.out.println(queue);
        //阻塞队列中获取元素使用take方法,会产生阻塞效果
        System.out.println("开始获取元素");
        System.out.println(queue.take());
        System.out.println(queue);
    }
}

insert image description here
When all the elements in the blocking queue are obtained, the blocking queue is empty at this time, and when continuing to obtain elements, it will enter the blocking state.

public class Demo01_BlockingQueue {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        //定义一个阻塞队列
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(3);
        //往队列中写入元素
        queue.put(1);
        queue.put(2);
        queue.put(3);
        System.out.println("已经插入了三个元素");
        System.out.println(queue);
        //阻塞队列中获取元素使用take方法,会产生阻塞效果
        System.out.println("开始获取元素");
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println("已经获取了三个元素");
        System.out.println(queue.take());
        System.out.println("已经获取了四个元素");
    }
}

insert image description here

Implement a blocking queue

When learning data structures, two data structures are used to implement a common queue bottom layer: circular array and linked list. A blocking queue is an operation that adds blocking and waiting to a normal queue. This blocking waiting operation is waiting (wait) and waking up (notify).
Determine the scope of locking:
These two methods are strongly related to synchronized, so to add synchronized, you must first determine the scope of locking. In the put() and take() methods, synchronized is added according to the scope of the modified shared variable. Since the entire method is modifying shared variables, the entire method is locked. If an object needs new to be used, then the lock object is generally this, and the lock object at this time is just this.
Determine the timing of waiting:
when adding elements, when the current array is full, block and wait at this time; when fetching elements, when the blocking queue is empty, block and wait at this time.
Determine the timing of wake-up:
In the method of adding elements, execute the wake-up operation after the last step of the put operation to wake up the thread that fetches elements; in the method of fetching elements, when the current queue has free space, wake up the thread that adds elements thread.

Code

public class MyBlockingQueue {
    
    
    //定义一个保存元素的数组
    private int[] elementData = new int[100];
    //定义队首下标
    private volatile int head;
    //定义队尾下标
    private volatile int tail;
    //定义有效元素的个数
    private volatile int size;


    //插入一个元素
    public void put(int value) throws InterruptedException {
    
    
        //根据修改共享变量的范围加锁
        //锁对象是this即可
        synchronized (this){
    
    
            //判断数组是否已满
            while(size >= elementData.length){
    
    
                //阻塞等待
                this.wait();
            }
            //向队尾插入元素
            elementData[tail] = value;
            //移动队尾下标
            tail ++;
            //修正队尾下标
            if (tail>=elementData.length) {
    
    
                tail = 0;
            }
            //修改有效元素个数
            size ++;
            //做唤醒操作
            this.notifyAll();
        }

    }

    //获取一个元素
    public int take() throws InterruptedException {
    
    
        //根据修改共享变量的范围加锁
        //锁对象是this即可
        synchronized (this){
    
    
            //判断队列是否为空
            while (size<=0) {
    
    
                this.wait();
            }
            //从队首出队
            int value = elementData[head];
            //移动队首下标
            head ++;
            //修正队首下标
            if (head>=elementData.length) {
    
    
                head = 0;
            }
            //修改有效元素个数
            size --;
            //唤醒操作
            this.notifyAll();
            //返回队首元素
            return value;
        }
    }
}

⚠️⚠️⚠️Note : In the official documentation of the wait method, it is stated: "Threads can wake up without notification, interruption or timeout, so-called spurious wakeups . Although it rarely happens in practice in this case, the application A program must guard against this by testing for the condition that caused the thread to be woken up, and continuing to wait if the condition is not met. In other words, the wait should always occur in a loop".
That is to say, when the wait condition is met for the first time, the thread enters the blocked state. After being awakened, many things will happen during this period. One possibility is that after being awakened, the waiting condition is still true, so the waiting condition needs to be checked again. If If satisfied, continue to block and wait. That is, when implementing the above blocking queue, when checking the judgment condition of wait, use while to judge instead of if .
insert image description here
Finally, in a multi-threaded environment, you need to add volatile to the shared variable.

Test custom blocking queue

public class Demo02_MyBlockingQueue {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        //定义一个阻塞队列
        MyBlockingQueue queue = new MyBlockingQueue();
        //往队列中写入元素
        queue.put(1);
        queue.put(2);
        queue.put(3);
        System.out.println("已经插入了三个元素");

        System.out.println("开始获取元素");
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println("已经获取了三个元素");
        System.out.println(queue.take());
        System.out.println("已经获取了四个元素");
    }
}

insert image description here

Implement the producer consumer model

Simulate the producer and the consumer with one thread respectively, and implement a simple producer-consumer model. Let the producer thread produce once every 10ms, and let the consumer thread consume again every 1s.
insert image description here
Code

public class Demo03_ProducerConsumer {
    
    
    // 定义一个阻塞队列,初始容量为100
    private static MyBlockingQueue queue = new MyBlockingQueue();

    public static void main(String[] args) {
    
    
        // 创建生产者线程
        Thread producer = new Thread(() -> {
    
    
            //记录消息编号
            int num = 1;
            while (true) {
    
    
                // 生产一条就打印一条日志
                System.out.println("生产了元素 " + num);
                try {
    
    
                    // 把消息放入阻塞队列中
                    queue.put(num);
                    num++;
                    // 休眠10ms
                    TimeUnit.MILLISECONDS.sleep(10);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        // 启动生产者
        producer.start();

        // 创建消费者线程
        Thread consumer = new Thread(() -> {
    
    
            while (true) {
    
    
                try {
    
    
                    // 从队列中获取元素(消息)
                    int num = queue.take();
                    // 打印一下消费日志
                    System.out.println("消费了元素 :" + num);
                    // 休眠1秒
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        // 启动消费者
        consumer.start();
    }
}

Test results:
Let the producer thread produce once every 10ms, and let the consumer thread consume again every 1s. At this time, the message in the blocking queue is full and then consumed, but the production and consumption are still carried out at the same time.
insert image description here

On the contrary, if the producing thread is made slow and the consuming thread is fast, one message will be consumed every time a message is produced. A blocking queue is never full.
insert image description here


Keep going~
insert image description here

Guess you like

Origin blog.csdn.net/qq_43243800/article/details/130961993