5

1. Coordinate production/consumption needs

The content of this article mainly wants to introduce the use of Lock combined with Condition. In order to better understand Lock lock and Condition lock signal, let's write an ArrayBlockingQueue . JDK actually already has this class, which is implemented based on Lock lock and Condition lock signal. Of course, the JDK implementation code is very complex, including more rigorous logic verification, and more work has been done from the perspective of performance optimization. In this article, we just briefly implement its core logic:

  • Specify the maximum capacity limit when ArrayBlockingQueue is initialized and constructed
  • Provides the put method. When the maximum upper limit of the Queue queue capacity is reached, the thread that produces data is blocked.
  • After the put method produces data, the queue is definitely not empty, and the consumer thread is notified to consume.
  • Provide the take method to block the thread that consumes data when the Queue queue capacity is 0.
  • After the take method is executed, the queue is definitely not full, and the producer thread is notified to produce.
  • A piece of data can only be taken once, after taking the data is deleted from the queue

I believe that after completing the above logic, you must master the Lock lock and Condition lock signal of Java concurrent programming! In fact, this logic is basically the implementation logic of Kafka producer client buffer queue and batch data transmission . The difference is that the take method takes out all the data in the buffer at one time, and the take method in this paper takes out one piece of data at a time.

Second, the construction method

The method of constructing the queue is very simple, use a List as the data storage queue, and specify its capacity. So far, we have not implemented capacity judgment and the function of blocking threads.

 //类成员变量-存储数据的队列
private List<Object> queue;
//类成员变量-存储队列的容量上限
private int queueSize;

public MyBlockingQueue(int queueSize) {
  this.queueSize = queueSize;
  queue = new ArrayList<>(queueSize);//存储消息的集合
}

3. Lock & Condition logic design

First of all, we must have a lock to ensure the synchronization of data put and take operations, that is: a piece of data can only be taken once, and the data will be deleted from the queue after taking; and the creation of Condition logic requires Lock locks. Students who have learned basic concurrent programming in java can understand the Lock lock as the Synchronized synchronization code block. The function is the same. I wrote a column "java concurrent programming" to introduce the difference between the two, welcome to pay attention.

 private Lock lock = new ReentrantLock();//锁

Condition logic can be understood as wait and notify in traditional JDK multi-threaded programming, but the semantics of Condition are easier to understand. As shown in the following code:

 private Condition notFull = lock.newCondition();   //队列不为满

notFull.signal();   //通知生产者队列不为满,可以继续生产数据(通常在消费者拿走数据之后,调用)
notFull.await();    //队列已满,阻塞生产线程(await对condition逻辑取反)
 private Condition notEmpty = lock.newCondition();  //队列不为空

notEmpty.signal();  //通知消费者线程队列不为空,可以继续消费数据(通常在生产者生产数据之后,调用)
notEmpty.await();  //队列已经空了,阻塞消费线程(await对condition逻辑取反)

When you use Lock & Condition for thread synchronization coordination, you must first design the logical semantics of the condition like me

  • Design the expression when xxxx as condition.
  • When the condition satisfies the condition, send the signal signal() to notify other threads;
  • When the situation is exactly the opposite of the condition, use await to block the current thread.

Fourth, put into the data

In fact, the most important thing is to complete the logic design of Lock & Condition, the rest is to fill in the blanks, the template is as follows

The while loop is used to determine whether the current capacity of the queue has reached the upper limit of capacity. If the upper limit is reached, the queue is full. When the queue is full ( notFull uses await ), await blocks the production thread and continues to put data into the queue. Here, a small partner once asked me a strange question: multiple threads hold the same lock, how do you know that the production thread is blocked, not the consumer thread? Answer: Whether a thread is a production thread or a consumer thread depends on its action (what method to call), and there is no label to define it. The thread that calls the put method to put data is the thread that produces the data. The while/await combination is a standard way of writing, please don't innovate and change it to if at will, otherwise you will encounter many strange bugs.

 //队列满了,await阻塞生产线程
while (queue.size() >= queueSize) {
  System.out.println(Thread.currentThread().getName() + "等待,因为队列满了" );
  notFull.await();
}

Add a piece of data to the queue, at this point we can determine that the queue is notEmpty, so use notEmpty.signal() to send a signal to the producer. Here comes the problem again: multiple threads hold the same lock, how do you know that the consumer thread is notified, not the producer thread? The answer is that I really don't know , so the above while (queue.size() >= queueSize) uses while instead of if. Even if the producer thread is awakened, the while judgment will block it from await.

 //向队列添加一条消息,同时通知消费者有新消息了
queue.add(message);
System.out.println(Thread.currentThread().getName() + "生产" + message );
notEmpty.signal();//通知消费者线程

Five, take consumption data

take takes out the data from the queue. After taking out the data, the queue must be notFull, so the notFull.signal signal is issued. When the queue is empty (notEmpty uses await to negate), await blocks the consumer thread at the same time.

 public Object take() throws InterruptedException {
  Object retVal = null;
  lock.lock();//操作队列先加锁
  try {
    //队列空了,通知生产线程,消费线程阻塞
    while (queue.size() == 0) {
      System.out.println("队列已经空了,停止消费!");
      notEmpty.await();
    }
    //队列删除一条消息,同时通知生产者队列有位置了
    retVal = queue.get(0);
    queue.remove(0);
    notFull.signal();   //同时通知生产者队列
  } finally {
    lock.unlock();
  }
  return retVal;
}

I believe that with the foundation of the put method above, it is very easy to understand the code in the take method. I will not explain too much here.

6. Production and consumption test

 public static void main(String[] args) {
  //为了方便查看测试结果,我们的队列容量设置小一些
  MyBlockingQueue queue  = new MyBlockingQueue(2);

  //生产者线程
  new Thread(()->{
    for(int i = 0;i < 5;i++){
      try {
        queue.put("msg" + i);  //放入5条数据
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }).start();

  //消费者线程
  new Thread(()->{
    while(true){  //一直消费
      try {
        System.out.println(Thread.currentThread().getName()
                + "消费数据" + queue.take());
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }).start();
}

The output is as follows, which meets our needs. When the queue is full, the producer thread Thread-0 waits; the production and consumption coordinate with each other to notify each other, and finally the data consumption is completed, the queue is empty, and the consumer thread is blocked.

 Thread-0生产msg0
Thread-0生产msg1
Thread-0等待,因为队列满了
Thread-1消费数据msg0
Thread-0生产msg2
Thread-0等待,因为队列满了
Thread-1消费数据msg1
Thread-0生产msg3
Thread-0等待,因为队列满了
Thread-1消费数据msg2
Thread-0生产msg4
Thread-1消费数据msg3
Thread-1消费数据msg4
队列已经空了,停止消费!

Welcome to my blog, more boutique knowledge collection

This article is reproduced to indicate the source (must be connected, not just text): Antetokounmpo blog-zimug.com

If you find it helpful, please like and share! Your support is my inexhaustible creative motivation! . In addition, the author has recently output the following fine content, looking forward to your attention.


字母哥博客
933 声望1.5k 粉丝