10 minutes of chatting about blocking queues under concurrent packages from implementation and usage scenarios

In the previous article, we talked about the thread pool , and the thread pool contains blocking queues.

In this article we mainly talk about blocking queues under concurrent packages

blocking queue

What is a queue?

The queue can be implemented as an array or a linked list. It can implement a first-in-first-out sequential queue or a first-in-last-out stack queue.

So what is a blocking queue?

In the classic producer/consumer model, producers put produced elements into a queue, and consumers consume elements from the queue.

When the queue is full, we will manually block the producer until the consumer consumes again and manually wake up the producer.

When the queue is empty, we will manually block the consumer until the producer produces again and manually wake up the consumer.

Since an ordinary queue is used in this process, we need to manually operate blocking and waking up to ensure the synchronization mechanism.

The blocking queue provides waiting/notification functions based on the queue, which is used for communication between threads and avoids thread competition deadlocks.

Producers can be seen as user threads that add tasks to the thread pool, while consumers are worker threads in the thread pool.

Blocks worker threads from acquiring tasks when the blocking queue is empty, blocks user threads from adding tasks to the queue when the blocking queue is full (create non-core threads, reject policy)

API

Blocking queue provides the following four APIs for adding and deleting elements. We commonly use blocking waiting/timeout blocking waiting APIs.

method name throw an exception Return true/false blocking wait Timeout blocking wait
Add to add(Object) offer(Object) put(Object) offer(Object,long,TimeUnit)
delete remove() poll() take() poll(long,TimeUnit)
  1. Throws an exception: if the queue is full, add will throw an exception IllegalStateExceptio; if the queue is empty, remove will throw an exception.NoSuchElementException
  2. Return value: If the queue is full, the offer returns false; if the queue is empty, poll returns null.
  3. Blocking wait: Put will block the thread when the queue is full, or take will block the thread when the queue is empty.
  4. Timeout blocking waiting: Add timeout waiting on the basis of blocking waiting and returning true/false (wait for a certain period of time and exit waiting)
Fairness and unfairness of blocking queues

What is fair and unfair about blocking queues?

When the blocking queue is full, if it is fair, then the blocked thread will get elements from the blocking queue in order, if it is unfair, then the opposite is true.

In fact, whether the blocking queue is fair or unfair depends on whether the lock of the blocking queue is fair.

Blocking queues generally use unfair locks by default.

ArrayBlockingQueue

You can tell from the name that it is implemented as an array. Let’s first take a look at what important fields it has.

 public class ArrayBlockingQueue<E> extends AbstractQueue<E>
         implements BlockingQueue<E>, java.io.Serializable {
 ​
     //存储元素的数组
     final Object[] items;
 ​
     //记录元素出队的下标
     int takeIndex;
 ​
     //记录元素入队的下标
     int putIndex;
 ​
     //队列中元素数量
     int count;
 ​
     //使用的锁
     final ReentrantLock lock;
 ​
     //出队的等待队列,作用于消费者
     private final Condition notEmpty;
 ​
     //入队的等待队列,作用于生产者
     private final Condition notFull;
     
 }

After reading the key fields, we can know: ArrayBlockingQueueimplemented by arrays, using reentrant locks under the concurrent package, and using two waiting queues to act as producers and consumers at the same time

Why do we need to use two subscript records when leaving the team and entering the team?

In fact, it is a circular array, and the size does not change after initialization. After viewing the source code, you will naturally understand that it is a circular array.

In the constructor, initialize the array capacity and use unfair locks

     public ArrayBlockingQueue(int capacity) {
         this(capacity, false);
     }
 ​
     public ArrayBlockingQueue(int capacity, boolean fair) {
         if (capacity <= 0)
             throw new IllegalArgumentException();
         this.items = new Object[capacity];
         //锁是否为公平锁
         lock = new ReentrantLock(fair);
         notEmpty = lock.newCondition();
         notFull =  lock.newCondition();
     }

The fairness of ArrayBlockingQueue is implemented by ReentrantLock

Let’s take a look at the methods of joining the team. The methods of joining the team are all similar. In this article, we will look at the methods that support timeout and response to interruption.

     public boolean offer(E e, long timeout, TimeUnit unit)
         throws InterruptedException {
         //检查空指针
         checkNotNull(e);
         //获取超时纳秒
         long nanos = unit.toNanos(timeout);
         final ReentrantLock lock = this.lock;
         //加锁
         lock.lockInterruptibly();
         try {
             //如果队列已满
             while (count == items.length) {
                 //超时则返回入队失败,否则生产者等待对应时间
                 if (nanos <= 0)
                     return false;
                 nanos = notFull.awaitNanos(nanos);
             }
             //入队
             enqueue(e);
             return true;
         } finally {
             //解锁
             lock.unlock();
         }
     }

Directly use reentrant locks to ensure synchronization. If the queue is full, it will be judged during this period whether it has timed out, and it will return if it times out. If it does not time out, wait; if it is not full, the enqueue method will be executed.

     private void enqueue(E x) {
         //队列数组
         final Object[] items = this.items;
         //往入队下标添加值
         items[putIndex] = x;
         //自增入队下标 如果已满则定位到0 成环
         if (++putIndex == items.length)
             putIndex = 0;
         //统计数量增加
         count++;
         //唤醒消费者
         notEmpty.signal();
     }

In joining the queue, the main tasks are to add elements, modify the subscript added next time, count the elements in the queue and wake up the consumer. At this point, it can be seen that its implementation is a circular array.

ArrayBlockingQueueThe blocking queue implemented by a circular array has a fixed capacity and does not support dynamic expansion. It uses unfair methods to ReertrantLockensure the atomicity of enqueue and dequeue operations. It uses two waiting queues to store waiting producers and consumers. It is suitable for situations where the concurrency is not sufficient. big scene

LinkedBlockingQueue

LinkedBlockingQueueJudging from the name, it is implemented using a linked list. Let’s take a look at its key fields.

 public class LinkedBlockingQueue<E> extends AbstractQueue<E>
         implements BlockingQueue<E>, java.io.Serializable {
     //节点
     static class Node<E> {
         //存储元素
         E item;
 ​
         //下一个节点
         Node<E> next;
         
         //...
     }
 ​
     //容量上限
     private final int capacity;
 ​
     //队列元素数量
     private final AtomicInteger count = new AtomicInteger();
 ​
     //头节点
     transient Node<E> head;
 ​
     //尾节点
     private transient Node<E> last;
 ​
     //出队的锁
     private final ReentrantLock takeLock = new ReentrantLock();
 ​
     //出队的等待队列
     private final Condition notEmpty = takeLock.newCondition();
 ​
     //入队的锁
     private final ReentrantLock putLock = new ReentrantLock();
 ​
     //入队的等待队列
     private final Condition notFull = putLock.newCondition();
 }

From the fields, we can know that it uses the nodes of a one-way linked list, and uses the head and tail nodes to record the head and tail of the queue, and it uses two locks and two waiting queues to act on the head and tail of the queue, which can increase concurrency performance compared ArrayBlockingQueuewith

There is a strange thing: locks are used, why do we use atomic classes to record the number of elements count?

This is because two locks act on the operations of enqueueing and dequeuing. Enqueueing and dequeuing may also be executed concurrently, and the count is modified at the same time. Therefore, an atomic class must be used to ensure the atomicity of the modified number.

The capacity needs to be set during initialization, otherwise it will be set to an unbounded blocking queue (the capacity is the maximum value of int)

When the consumption speed is lower than the production speed, tasks will accumulate in the blocking queue, which will easily cause OOM.

     public LinkedBlockingQueue() {
         this(Integer.MAX_VALUE);
     }
 ​
     public LinkedBlockingQueue(int capacity) {
         if (capacity <= 0) throw new IllegalArgumentException();
         this.capacity = capacity;
         last = head = new Node<E>(null);
     }

Let’s take a look at the joining operation

     public boolean offer(E e, long timeout, TimeUnit unit)
         throws InterruptedException {
 ​
         if (e == null) throw new NullPointerException();
         long nanos = unit.toNanos(timeout);
         int c = -1;
         final ReentrantLock putLock = this.putLock;
         final AtomicInteger count = this.count;
         //加锁
         putLock.lockInterruptibly();
         try {
             //队列已满,超时返回,不超时等待
             while (count.get() == capacity) {
                 if (nanos <= 0)
                     return false;
                 nanos = notFull.awaitNanos(nanos);
             }
             //入队
             enqueue(new Node<E>(e));
             // 先获取再自增 c中存储的是旧值
             c = count.getAndIncrement();
             //如果数量没满 唤醒生产者
             if (c + 1 < capacity)
                 notFull.signal();
         } finally {
             //解锁
             putLock.unlock();
         }
         //如果旧值为0 说明该入队操作前是空队列,唤醒消费者来消费
         if (c == 0)
             signalNotEmpty();
         return true;
     }

The enqueue operation is similar, except that during this period, if the quantity is not full, the producer will be awakened to produce, and the queue is empty, and the consumer will be awakened to consume, thereby increasing concurrency performance.

Joining the team only changes the pointing relationship

     //添加节点到末尾
     private void enqueue(Node<E> node) {
         last = last.next = node;
     }

The lock must be acquired before waking up the consumer

     private void signalNotEmpty() {
         final ReentrantLock takeLock = this.takeLock;
         takeLock.lock();
         try {
             notEmpty.signal();
         } finally {
             takeLock.unlock();
         }
     }

The dequeue operation is also similar

     public E poll(long timeout, TimeUnit unit) throws InterruptedException {
         E x = null;
         int c = -1;
         long nanos = unit.toNanos(timeout);
         final AtomicInteger count = this.count;
         final ReentrantLock takeLock = this.takeLock;
         takeLock.lockInterruptibly();
         try {
             // 队列为空 超时返回空,否则等待
             while (count.get() == 0) {
                 if (nanos <= 0)
                     return null;
                 nanos = notEmpty.awaitNanos(nanos);
             }
             //出队
             x = dequeue();
             c = count.getAndDecrement();
             //队列中除了当前线程获取的任务外还有任务就去唤醒消费者消费
             if (c > 1)
                 notEmpty.signal();
         } finally {
             takeLock.unlock();
         }
         //原来队列已满就去唤醒生产者 生产
         if (c == capacity)
             signalNotFull();
         return x;
     }

LinkedBlockingQueueSimilar to the implementation of dequeue ArrayBlockingQueueand enqueue

It's just that LinkedBlockingQueuethe locks acquired/released when entering and leaving the queue are different, and during this process, other producers and consumers are awakened under different circumstances to further improve concurrency performance.

LinkedBlockingQueue is a blocking queue implemented by a one-way linked list, recording the first and last nodes; the default is an unbounded and unfair blocking queue (the capacity must be set during initialization otherwise it may be OOM), using two locks and two waiting queues to operate the entry and exit operations respectively. The producers and consumers of the team will also wake up the producers and consumers under different circumstances during the queue entry and exit operations, thereby further improving the concurrency performance and being suitable for scenarios with large concurrency.

LinkedBlockingDeque

LinkedBlockingDequeThe implementation is LinkedBlockQueuesimilar to that, LinkedBlockQueueand supports adding and deleting operations from the head and tail of the queue on the basis of

It is a doubly linked list with a series of First and Last methods, such as: offerLast,pollFirst

Because it LinkedBlockingDequeis bidirectional, it is often used to implement work-stealing algorithms to reduce thread competition.

What is a work-stealing algorithm?

For example, multi-threads process tasks in multiple blocking queues (one-to-one correspondence). Each thread obtains task processing from the head of the queue. When thread A finishes processing all tasks in the blocking queue it is responsible for, it steals tasks from other blocking queues from the tail of the queue. tasks, so that no competition will occur unless there is only one task left in the queue.

ForkJoinThe framework uses it to act as a blocking queue. We will talk about this framework later.

PriorityBlockingQueue

PriorityBlockingQueue is an unbounded blocking queue sorted by priority. The blocking queue is sorted according to priority.

Using heap sort, the specific sorting algorithm is implemented by Comparableor Comparatorcomparison rules

  1. Default: Objects in generics need to implement Comparablecomparison rules and sort according to the compareTo method rules.
  2. The comparator specified in the constructor Comparatoris sorted according to the comparator rules.
     @Test
     public void testPriorityBlockingQeque() {
         //默认使用Integer实现Comparable的升序
         PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue<>(6);
         queue.offer(99);
         queue.offer(1099);
         queue.offer(299);
         queue.offer(992);
         queue.offer(99288);
         queue.offer(995);
         //99 299 992 995 1099 99288
         while (!queue.isEmpty()){
             System.out.print(" "+queue.poll());
         }
 ​
         System.out.println();
         //指定Comparator 降序
         queue = new PriorityBlockingQueue<>(6, (o1, o2) -> o2-o1);
         queue.offer(99);
         queue.offer(1099);
         queue.offer(299);
         queue.offer(992);
         queue.offer(99288);
         queue.offer(995);
         //99288 1099 995 992 299 99
         while (!queue.isEmpty()){
             System.out.print(" "+queue.poll());
         }
     }

Suitable for scenarios that need to be processed according to priority

DelayQueue

Delay is an unbounded blocking queue for delayed acquisition of elements. The longest delay is at the end of the queue.

The Delay queue element implements the Delayed interface to getDelayobtain the delay time

 public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
     implements BlockingQueue<E> {
 }
 ​
 public interface Delayed extends Comparable<Delayed> {
     long getDelay(TimeUnit unit);
 }

DelayQueue application scenarios

  1. Design of the cache system: DelayQueue stores the cache validity period. When the element can be obtained, the cache has expired.
  2. Scheduled task scheduling: Set the time of the scheduled task as the delay time, and start execution once the task can be obtained

ScheduledThreadPoolExecutorTaking the scheduled task of the scheduled thread pool ScheduledFutureTaskas an example, it implements Delayedobtaining the delayed execution time

image.png

  1. When creating an object, initialize data

             ScheduledFutureTask(Runnable r, V result, long ns, long period) {
                 super(r, result);
                 //time记录当前对象延迟到什么时候可以使用,单位是纳秒
                 this.time = ns;
                 this.period = period;
                 //sequenceNumber记录元素在队列中先后顺序  sequencer原子自增
                 //AtomicLong sequencer = new AtomicLong();
                 this.sequenceNumber = sequencer.getAndIncrement();
             }
    
  2. Implement the getDelay method of the Delayed interface

     public long getDelay(TimeUnit unit) {
         return unit.convert(time - now(), NANOSECONDS);
     }
    
  3. The Delay interface inherits the Comparable interface, and the purpose is to implement the compareTo method to continue sorting

             public int compareTo(Delayed other) {
                 if (other == this) // compare zero if same object
                     return 0;
                 if (other instanceof ScheduledFutureTask) {
                     ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
                     long diff = time - x.time;
                     if (diff < 0)
                         return -1;
                     else if (diff > 0)
                         return 1;
                     else if (sequenceNumber < x.sequenceNumber)
                         return -1;
                     else
                         return 1;
                 }
                 long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
                 return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
             }
    

SynchronousQueue

SynchronousQueue is a blocking queue that supports unfairness and does not store elements by default.

Each put operation must wait for a take operation, otherwise elements cannot be added and will be blocked.

Use fair lock

     @Test
     public void testSynchronousQueue() throws InterruptedException {
         final SynchronousQueue<Integer> queue = new SynchronousQueue(true);
         new Thread(() -> {
             try {
                 queue.put(1);
                 queue.put(2);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }, "put12线程").start();
 ​
         new Thread(() -> {
             try {
                 queue.put(3);
                 queue.put(4);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }, "put34线程").start();
 ​
         TimeUnit.SECONDS.sleep(1);
         System.out.println(Thread.currentThread().getName() + "拿出" + queue.take());
         TimeUnit.SECONDS.sleep(1);
         System.out.println(Thread.currentThread().getName() + "拿出" + queue.take());
         TimeUnit.SECONDS.sleep(1);
         System.out.println(Thread.currentThread().getName() + "拿出" + queue.take());
         TimeUnit.SECONDS.sleep(1);
         System.out.println(Thread.currentThread().getName() + "拿出" + queue.take());
     }
 //结果 因为使用公平锁 1在2前,3在4前
 //main拿出1
 //main拿出3
 //main拿出2
 //main拿出4

The SynchronousQueue queue itself does not store elements, but is responsible for delivering data from producers to consumers. It is suitable for transitive scenarios.

In this scenario, the throughput will be higher than ArrayBlockingQueue and LinkedBlockingQueue.

LinkedTransferQueue

LinkedTransferQueue is an unbounded blocking queue composed of a linked list, with transfer()and tryTransfer()methods

transfer()

If there is a consumer waiting to receive elements, transfer(e) will transfer element e to the consumer

If there is no consumer waiting to receive the element, transfer(e) will store the element e at the end of the queue and will not return until a consumer obtains it.

     @Test
     public void testTransfer() throws InterruptedException {
         LinkedTransferQueue queue = new LinkedTransferQueue();
         new Thread(()->{
             try {
                 //阻塞直到被获取
                 queue.transfer(1);
                 //生产者放入的1被取走了
                 System.out.println(Thread.currentThread().getName()+"放入的1被取走了");
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         },"生产者").start();
 ​
         TimeUnit.SECONDS.sleep(3);
         //main取出队列中的元素
         System.out.println(Thread.currentThread().getName()+"取出队列中的元素");
         queue.poll();
     }

tryTransfer()Return directly regardless of whether the consumer consumes or not

     @Test
     public void testTryTransfer() throws InterruptedException {
         LinkedTransferQueue<Integer> queue = new LinkedTransferQueue<>();
         //false
         System.out.println(queue.tryTransfer(1));
         //null
         System.out.println(queue.poll());
 ​
         new Thread(()->{
             try {
                 //消费者取出2
                 System.out.println(Thread.currentThread().getName()+"取出"+queue.poll(2, TimeUnit.SECONDS));
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         },"消费者").start();
         TimeUnit.SECONDS.sleep(1);
         //true
         System.out.println(queue.tryTransfer(2));
     }

tryTransfer(long,TimeUnit)When the consumer consumes elements within the timeout period, it returns true, otherwise it returns false.

Summarize

ArrayBlockingQueue is implemented by a ring array. The fixed capacity cannot be expanded. It uses unfair reentrant locks and two waiting queue operations for enqueue and dequeue operations. It is suitable for scenarios with small concurrency.

LinkedBlockingQueue is implemented by a one-way linked list and is unbounded by default. It uses two reentrant locks and two waiting queues for enqueue and dequeue operations, and may wake up producer or consumer threads during this period to improve concurrency performance.

LinkedBlockingDeque is implemented by a two-way linked list. Based on LinkedBlockingQueue, it can add and delete operations at the head and tail of the queue, and is suitable for work stealing algorithm 1

PriorityBlockingQueue is a priority queue implemented by heap sorting. The specific sorting algorithm is implemented by Comparable and Comparator. It is suitable for scenarios where tasks need to be processed according to priority sorting.

DelayQueue is a delay queue. The elements stored in the queue need to implement Delayedan interface to obtain the delay time. It is suitable for cache invalidation and scheduled tasks.

SynchronousQueue does not store elements, but only delivers elements produced by producers to consumers. It is suitable for transitive scenarios, such as transferring data between different threads.

LinkedTransgerQueue is a transmission-shaped blocking queue, suitable for scenarios where a single element is transferred

When using an unbounded blocking queue, you need to set the capacity to avoid OOM caused by too many storage tasks.

Finally (don’t do it for free, just press three times in a row to beg for help~)

This article is included in the column " From Point to Line, and from Line to Surface" to build a Java concurrent programming knowledge system in simple terms . Interested students can continue to pay attention.

The notes and cases of this article have been included in gitee-StudyJava and github-StudyJava . Interested students can continue to pay attention under stat~

Case address:

Gitee-JavaConcurrentProgramming/src/main/java/E_BlockQueue

Github-JavaConcurrentProgramming/src/main/java/E_BlockQueue

If you have any questions, you can discuss them in the comment area. If you think Cai Cai’s writing is good, you can like, follow, and collect it to support it~

Follow Cai Cai and share more useful information, public account: Cai Cai’s back-end private kitchen

This article is published by OpenWrite, a blog that publishes multiple articles !

Lei Jun: The official version of Xiaomi’s new operating system ThePaper OS has been packaged. A pop-up window on the Gome App lottery page insults its founder. The U.S. government restricts the export of NVIDIA H800 GPU to China. The Xiaomi ThePaper OS interface is exposed. A master used Scratch to rub the RISC-V simulator and it ran successfully. Linux kernel RustDesk remote desktop 1.2.3 released, enhanced Wayland support After unplugging the Logitech USB receiver, the Linux kernel crashed DHH sharp review of "packaging tools": the front end does not need to be built at all (No Build) JetBrains launches Writerside to create technical documentation Tools for Node.js 21 officially released
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/6903207/blog/10109173