It is not allowed to have Java programmers who do not understand the implementation principle of BlockingQueue blocking queue

Continue to create, accelerate growth! This is the 4th day of my participation in the "Nuggets Daily New Plan · October Update Challenge", click to view the details of the event

We seem to rarely use BlockingQueue in our usual development. For example, when we want to store a set of data, we will use ArrayList, and if we want to store key-value pair data, we will use HashMap. In what scenarios do we need to use BlockingQueue?

1. Application scenarios of BlockingQueue

After we process a batch of data, we need to send this batch of data to the downstream method for further processing, but the processing rate of the downstream method is not controlled, and it may be fast or slow. If the processing rate of the downstream method is slow, which slows down the processing rate of the current method, what should I do at this time?

You may think of using a thread pool, which is a solution, but you need to create a lot of threads, and consider that downstream methods do not support concurrency. If it is a CPU-intensive task, multi-threaded processing may be slower than single-threaded processing because frequent context switching is required.

At this time, you can consider using BlockingQueue. The most typical application scenario of BlockingQueue is the above producer-consumer model. The producer puts data into the queue, the consumer takes data from the queue, and uses BlockingQueue as a buffer queue in the middle, which solves the problem of asynchronous speed between the producer and the consumer.

image-20221003203703573.png

You may think of the message queue (MessageQueue), the message queue is equivalent to a distributed blocking queue, and the BlockingQueue is equivalent to a local blocking queue, which only works on this machine. Corresponding to distributed cache (such as: Redis, Memcache) and local cache (such as: Guava, Caffeine).

In addition, there are shadows of BlockingQueue in many frameworks. For example, in the thread pool, BlockingQueue is used to buffer tasks. The methods of sending and pulling messages in the message queue are also borrowed from BlockingQueue, and they are very similar to use.

Today, let's analyze the underlying source code of Queue in depth.

2. Usage of BlockingQueue

The usage of BlockingQueue is very simple, that is, to put data and get data.

/**
 * @apiNote BlockingQueue示例
 * @author 一灯架构
 */
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        // 1. 创建队列,设置容量是10
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
        // 2. 往队列中放数据
        queue.put(1);
        // 3. 从队列中取数据
        Integer result = queue.take();
    }
}
复制代码

In order to meet different usage scenarios, BlockingQueue has designed many methods for putting data and removing data.

operate Throw an exception return a specific value block block for a while
put data add offer put offer(e, time, unit)
fetch data remove poll take poll(time, unit)
Fetch data (do not delete) element() peek() not support not support

The differences between these groups of methods are:

  1. When the queue is full, and then put data into the queue, the add method throws an exception, the offer method returns false, the put method will keep blocking (until another thread takes the data from the queue), the offer method blocks the specified time and then returns false.
  2. When the queue is empty and data is taken from the queue, the remove method throws an exception, the poll method returns null, the take method will block (until other threads put data into the queue), the poll method blocks for a specified time and then returns null.
  3. When the queue is empty, and then go to the queue to view the data (without deleting the data), the element method throws an exception, and the peek method returns null.

The most used methods in work are the methods of offer and poll that block for a specified time.

3. BlockingQueue implementation class

BlockingQueue commonly has the following five implementation classes, mainly due to different application scenarios.

  • ArrayBlockingQueue

    The blocking queue implemented based on arrays needs to specify the capacity size when creating the queue, which is a bounded queue.

  • LinkedBlockingQueue

    The blocking queue implemented based on linked list, the default is unbounded queue, the capacity can be specified when creating

  • SynchronousQueue

    An unbuffered blocking queue, the data produced needs to be consumed immediately

  • PriorityBlockingQueue

    The blocking queue with priority is implemented, based on data display, it is an unbounded queue

  • DelayQueue

    The blocking queue that implements the delay function, based on PriorityQueue, is an unbounded queue

4. BlockingQueue source code analysis

The five subclasses of BlockingQueue are implemented in the same way. This time, the most commonly used ArrayBlockingQueue is used for source code analysis.

4.1 ArrayBlockingQueue class properties

Let's take a look at what properties are in the ArrayBlockingQueue class:

// 用来存放数据的数组
final Object[] items;

// 下次取数据的数组下标位置
int takeIndex;

// 下次放数据的数组下标位置
int putIndex;

// 当前已有元素的个数
int count;

// 独占锁,用来保证存取数据安全
final ReentrantLock lock;

// 取数据的条件
private final Condition notEmpty;

// 放数据的条件
private final Condition notFull;
复制代码

The implementation of the four groups of data access methods in ArrayBlockingQueue is also similar, and this time, the put and take methods are used for analysis.

4.2 Source code analysis of put method

image-20221003220940753.png

Whether it is putting data or fetching data, it starts from the head of the line and gradually moves to the end of the line.

// 放数据,如果队列已满,就一直阻塞,直到有其他线程从队列中取走数据
public void put(E e) throws InterruptedException {
    // 校验元素不能为空
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
  	// 加锁,加可中断的锁
    lock.lockInterruptibly();
    try {
        // 如果队列已满,就一直阻塞,直到被唤醒
        while (count == items.length)
            notFull.await();
      	// 如果队列未满,就往队列添加元素
        enqueue(e);
    } finally {
      	// 结束后,别忘了释放锁
        lock.unlock();
    }
}

// 实际往队列添加数据的方法
private void enqueue(E x) {
    // 获取数组
    final Object[] items = this.items;
    // putIndex 表示本次插入的位置
    items[putIndex] = x;
    // ++putIndex 计算下次插入的位置
    // 如果下次插入的位置,正好等于队尾,下次插入就从 0 开始
    if (++putIndex == items.length)
        putIndex = 0;
  	// 元素数量加一
    count++;
    // 唤醒因为队列空等待的线程
    notEmpty.signal();
}
复制代码

There is an interesting design in the source code. When adding elements, if the end of the queue is reached, the next time it is added from the head of the queue, which is equivalent to making a circular queue.

Like the following:

image-20221003222232640.png

4.3 source code of take method

// 取数据,如果队列为空,就一直阻塞,直到有其他线程往队列中放数据
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
  	// 加锁,加可中断的锁
    lock.lockInterruptibly();
    try {
        // 如果队列为空,就一直阻塞,直到被唤醒
        while (count == 0)
            notEmpty.await();
        // 如果队列不为空,就从队列取数据
        return dequeue();
    } finally {
      	// 结束后,别忘了释放锁
        lock.unlock();
    }
}

// 实际从队列取数据的方法
private E dequeue() {
  	// 获取数组
    final Object[] items = this.items;
    // takeIndex 表示本次取数据的位置,是上一次取数据时计算好的
    E x = (E) items[takeIndex];
    // 取完之后,就把队列该位置的元素删除
    items[takeIndex] = null;
    // ++takeIndex 计算下次拿数据的位置
    // 如果正好等于队尾的话,下次就从 0 开始拿数据
    if (++takeIndex == items.length)
        takeIndex = 0;
    // 元素数量减一
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    // 唤醒被队列满所阻塞的线程
    notFull.signal();
    return x;
}
复制代码

4.4 Summary

  1. ArrayBlockingQueue is a blocking queue implemented based on arrays. When creating a queue, you need to specify the capacity and size. It is a bounded queue.
  2. The bottom layer of ArrayBlockingQueue is in the form of a circular queue to ensure that the array position can be reused.
  3. ArrayBlockingQueue access is locked by ReentrantLock to ensure thread safety and can be used with confidence in a multi-threaded environment.
  4. When using ArrayBlockingQueue, estimate the queue length to ensure that the producer and consumer rates match.

I am "One Light Architecture". If this article is helpful to you, you are welcome to like, comment and pay attention to your friends. Thank you old irons. See you in the next issue

Guess you like

Origin juejin.im/post/7150297762783838222
Recommended