Blocking Queue and Array BlockingQueue Source Parsing

Keywords: Java

What is a blocking queue

When the queue is empty, the operation of fetching elements from the total queue will be blocked, and when the queue is full, the operation of adding elements to the queue will be blocked. Threads trying to retrieve elements from an empty blocked queue will be blocked until other threads insert new elements into the queue. Similarly, threads that attempt to add new elements to a full queue are blocked until other threads make the queue free again.

Processing methods throw Returns a special value Constant blockage Overtime Exit
Insertion method add(e) offer(e) put() offer(e, time, unit)
Remove Method remove() poll(e) take() poll(time, unit)
Inspection methods element() peek() nothing nothing
  • Throw an exception: When the queue is full, then insert an element into the queue, an IllegalStateException exception is thrown. When the queue is empty and the element is retrieved from the queue, the NoSuchElementException exception is thrown.
  • Returns a special value: When the queue is full, adding elements to the queue returns false, otherwise returns true. When the queue is empty, get the element from the queue, return null, otherwise return the element.
  • Continuous blocking: When the blocking queue is full, if the producer inserts elements into the queue, the queue will block the current thread until the queue is available or the response interrupt exits. When the blocking queue is empty, if the consumer thread fetches data from the blocking queue, the queue will block the current thread until the queue is idle or the response interrupt exits.
  • Timeout Exit: When the queue is full, if the production line adds elements to the queue, the queue will block the production line for a period of time, and exit will return false if the queue exceeds the specified time. When the queue is empty, the consumer thread removes the elements from the queue, and the queue blocks for a period of time. If the queue exits beyond the specified time, it returns null.

Blocking queues in java

  1. Array Blocking Queue: A bounded queue consisting of an array structure. This queue is sorted in FIFO order. Supports fair locks and unfair locks.
  2. LinkedBlockingQueue: A bounded queue consisting of a linked list structure with the length of Integer.MAX_VALUE. This queue is sorted in FIFO order.
  3. Priority Blocking Queue: An unbounded queue that supports thread priority ordering. It defaults to natural ordering. It can also customize the implementation of compareTo() method to specify element ordering rules, which can not guarantee the order of elements with the same priority.
  4. DelayQueue: An unbounded queue that implements Priority BlockingQueue for delayed acquisition. When creating an element, you can specify how long it takes to get the current element from the queue. Elements can be retrieved from the queue only after the delay has expired.
  5. SynchronousQueue: A blocking queue that does not store elements. Each put operation must wait for the take operation, otherwise it cannot add elements. Supports fair locks and unfair locks.
  6. LinkedTransferQueue: An unbounded blocking queue consisting of a linked list structure, equivalent to other queues. LinkedTransferQueue queues have more transfer and tryTransfer methods.

    • Transfer: If a consumer thread is currently acquiring an element, transfer passes the element directly to the consumer thread, otherwise joins the queue until the element is consumed.
    • TryTransfer: If there is current consumption, which is acquiring elements, tryTransfer passes the elements directly to the consumer thread, otherwise it returns false immediately.
  7. Linked Blocking Queue: A bidirectional blocking queue consisting of a linked list structure. Elements can be added and removed at both the head and tail of the queue, and lock competition can be reduced to at most half when multiple threads are concurrent.

Source code parsing of Array BlockingQueue

The structure of the ArrayBlockingQueue class is as follows:

    public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
        private static final long serialVersionUID = -817911632652898426L;
        final Object[] items;  //Containers for storing elements with data
        int takeIndex;  //The next place to read or remove (remove, poll, take)
        int putIndex;  //Next place to store elements (add, offer, put)
        int count;  //The total number of elements in a queue
        final ReentrantLock lock;  //All access protection locks
        private final Condition notEmpty;  //Conditions waiting to get elements
        private final Condition notFull;  //Conditions to wait for elements to be stored
        //Slightly...

You can see that Array BlockingQueue uses final-modified object arrays to store elements, and once the arrays are initialized, the size of the arrays can't be changed. ReentrantLock locks are used to ensure lock contention, and Condition s are used to control whether threads block when inserting or acquiring elements.

  public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        //Get locks that support response interruption
        lock.lockInterruptibly();
        try {
            //Use the while loop to determine whether the queue is full to prevent false wake-up
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

The lock is obtained first, and then the queue is determined whether it is full or not. If it is full, the current generated thread is blocked until it is awakened when it is idle in the queue. When the queue is idle, it calls enqueue to insert elements.

private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        //Insert the current element into the array
        items[putIndex] = x;
        //Here you can see that this array is a ring array.
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        // Wake up the thread waiting on the notEmpty condition 
        notEmpty.signal();
    }

By inserting elements into the queue, you can see that the array in the queue is a ring array structure, so that each insertion and removal does not need to copy the elements in the moving array.

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        //Obtain a respondable interrupt lock
        lock.lockInterruptibly();
        try {
            //Use the while loop to determine whether the queue is full to prevent false wake-up
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

Consumer threads retrieve elements from blocked queues. If the elements in the queue are empty, the current consumer threads are blocked until data is available to call the dequeue method to retrieve elements. Otherwise, call the dequeue method directly to get the element

    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        //Get elements
        E x = (E) items[takeIndex];
        //Set the current location element to null
        items[takeIndex] = null;
        //Here you can see that this array is a ring array.
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            //Modifying iterator parameters
            itrs.elementDequeued();
        // Wake up the thread waiting on the notFull condition 
        notFull.signal();
        return x;
    }
ยทยทยท
//Get the elements of items[takeIndex] directly from the data, and set the element of the current position to null, and set the coordinates of the next takeIndex (++ takeIndex), the total number of queue elements - 1 and other operations.

```java
    public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        //Get a lock that does not respond to interrupts
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                //
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

First, determine whether the elements in the queue are full or not. If they are full, return false directly. Otherwise, call the enqueue method to insert the elements into the queue, and insert successfully returns true.

    public E poll() {
        final ReentrantLock lock = this.lock;
        //Get a lock that does not respond to interrupts
        lock.lock();
        try {
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }

Determine whether the queue is empty, if null is returned for empty, otherwise the dequeue method is called to return the element.

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

First, the offer method is called to insert the element, and the insertion returns true successfully. Otherwise, an IllegalStateException exception is thrown.

    public E remove() {
        E x = poll();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }

First, the poll method is called to get the element. If it is not empty, it returns directly. Otherwise, the NoSuchElementException exception is thrown.

    public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        checkNotNull(e);
        //Get the timeout time
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        //Get a lock that responds to interrupts
        lock.lockInterruptibly();
        try {
            while (count == items.length) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }

First, determine whether the queue is full or not. If the queue is full and recycled to determine whether the timeout time is timeout, the timeout will return to false directly. Otherwise, the nanos time of the production line will be blocked. If the nanos time is awakened, the enqueue method will be called to insert the element. If the queue is not satisfied, the enqueue method is called directly to insert the element and return true.

    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        //Get the timeout time
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        //Get a lock that responds to interrupts
        lock.lockInterruptibly();
        try {
            while (count == 0) {
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

First, the queue is judged to be empty by a loop. If the queue is empty, then the timeout is judged to be time-out, and the timeout is returned to null. Wait without timeout, and call the dequeue method to get the element when awakened at nanos time.

    public E element() {
        E x = peek();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }

Call the peek method to get the element. If the element is not empty, it returns. Otherwise, a NoSuchElementException exception is thrown.

    public E peek() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return itemAt(takeIndex); // null when queue is empty
        } finally {
            lock.unlock();
        }
    }

    final E itemAt(int i) {
        return (E) items[i];
    }

Call the itemAt method to get the element.

Other congestion queue implementations are similar in principle, using ReentrantLock and Condition s to complete concurrency control and blocking.

Posted by CodeEye on Mon, 08 Jul 2019 17:26:07 -0700