[big data Java foundation - Java concurrency 07] J.U.C's blocking queue: ArrayBlockingQueue

Keywords: Java C Big Data

ArrayBlockingQueue, a bounded blocking queue implemented by an array. The queue uses the FIFO principle to sort and add elements.

ArrayBlockingQueue is bounded and fixed. Its size is determined by the constructor during construction. It cannot be changed after confirmation. ArrayBlockingQueue supports the optional fair policy of sorting waiting producer threads and consumer threads. However, fair access of threads is not guaranteed by default. Fair policy can be selected during construction (fair = true). Fairness usually reduces throughput, but reduces variability and avoids "imbalance".

ArrayBlockingQueue

Let's first look at the definition of ArrayBlockingQueue:

public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, Serializable {
        private static final long serialVersionUID = -817911632652898426L;
        final Object[] items;
        int takeIndex;
        int putIndex;
        int count;
        // Reentry lock
        final ReentrantLock lock;
        // notEmpty condition
        private final Condition notEmpty;
        // notFull condition
        private final Condition notFull;
        transient ArrayBlockingQueue.Itrs itrs;
    }

You can clearly see that ArrayBlockingQueue inherits AbstractQueue and implements the BlockingQueue interface. Students who have seen the source code of java.util package should know AbstractQueue. Changing the class plays a very important role in the queue interface. This class provides the backbone implementation of queue operation (the specific content is moved to its source code). BlockingQueue inherits java.util.Queue as the core interface of blocking queue and provides out of line and in line operations in multi-threaded environment. As a user, you don't need to care when the queue blocks threads and wakes up threads. Everything is completed by BlockingQueue.

ArrayBlockingQueue uses ReentrantLock + Condition internally to complete concurrent operations in a multithreaded environment.

  • items, a fixed length array, maintains the elements of ArrayBlockingQueue
  • takeIndex, int, is the first position of ArrayBlockingQueue
  • putIndex, int, ArrayBlockingQueue tail position
  • count, number of elements
  • Lock, lock and ArrayBlockingQueue must acquire the lock when they are listed. The two steps share a lock
  • notEmpty, listing condition
  • notFull, listing conditions

Join the team

ArrayBlockingQueue provides many methods to add elements to the end of the queue.

  • Add (E): insert the specified element into the end of this queue (if it is immediately feasible and will not exceed the capacity of the queue), return true when successful, and throw IllegalStateException if the queue is full
  • Offer (E): insert the specified element into the end of this queue (if it is immediately feasible and will not exceed the capacity of the queue). It returns true when it is successful. If the queue is full, it returns false
  • Offer (E, long timeout, timeunit unit): insert the specified element into the end of the queue. If the queue is full, wait for available space before reaching the specified waiting time
  • Put (E): insert the specified element at the end of this queue. If the queue is full, wait for available space

There are many methods, so let's analyze one method: add (E):

public boolean add(E e) {
        return super.add(e);
    }

    public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

The add method calls offer(E e). If false is returned, an IllegalStateException will be thrown directly. Offer (E) is implemented for ArrayBlockingQueue:

 public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

Method first checks whether it is null, and then obtains the lock lock. After obtaining the lock successfully, if the queue is full, it will directly return false. Otherwise, enqueue(E e) will be called. enqueue(E e) is the core method listed. All listed methods will eventually call this method to insert elements at the end of the queue:

 private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }

This method is to add elements at the place where putIndex (tail) is known, and finally use the signal() method of notEmpty to notify the thread blocking the dequeue (if the queue is empty, the dequeue operation will be blocked).

Out of the team

The queueing method provided by ArrayBlockingQueue is as follows:

  • poll(): get and remove the header of the queue. If the queue is empty, null will be returned
  • poll(long timeout, TimeUnit unit): get and remove the header of this queue and wait for available elements before the specified waiting time (if necessary)
  • remove(Object o): removes a single instance (if any) of the specified element from this queue
  • take(): get and remove the header of this queue and wait (if necessary) until the element becomes available

poll()

 public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }

If the queue is empty, null is returned. Otherwise, dequeue() is called to get the column header element:

 private E dequeue() {
        final Object[] items = this.items;
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }

This method mainly takes out the elements from the column header (takeIndex position). At the same time, if the iterator itrs is not null, you need to maintain the iterator. Finally, call notFull.signal() to wake up the column thread.

take()

 public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

One difference between take() and poll() is the processing when count == 0. poll() directly returns null, while take() waits on notEmpty until the listed thread wakes up.

Posted by dcmbrown on Wed, 27 Oct 2021 08:26:28 -0700