[concurrent programming] a bounded blocking queue ArrayBlockingQueue based on array structure

Posted by phpmoron on Fri, 04 Feb 2022 14:51:43 +0100

What is ArrayBlockingQueue

  • ArrayBlockingQueue is the most typical bounded blocking queue.
  • Internal use of array storage elements!
  • The capacity size needs to be specified during initialization.
  • Using ReentrantLock to achieve thread safety!

Applicable scenarios for ArrayBlockingQueue

  • When used in the producer consumer model, ArrayBlockingQueue can be used if the production speed and consumption speed basically match.
  • If the production speed is much higher than the consumption speed, the queue will be filled and a large number of production threads will be blocked.

Implementation principle of ArrayBlockingQueue

  • The exclusive lock ReentrantLock is used to realize thread safety. The same lock object is used for queue in and queue out operations, that is, only one thread can perform queue in or queue out operations;
  • It means that producers and consumers cannot operate in parallel, which will become a performance bottleneck in high concurrency scenarios.

Characteristics of ArrayBlockingQueue

  • Bounded queue! fifo! Access is mutually exclusive!
  • The data structure used is a static array: the capacity is fixed and there is no capacity expansion mechanism; Positions without elements also occupy space and are occupied by null;
  • Use ReentrantLock lock: the access is the same lock, the operation is the same array object, and the access is mutually exclusive.

Inbound and outbound operations of ArrayBlockingQueue

  • Both pointers move from the head of the team to the tail of the team to ensure the first in first out principle of the queue!
  • Queue blocking object notFull: queue count=length. When the element cannot be put in, it will be blocked on this object.
  • Out of queue blocking object notEmpty: queue count=0. When no element is available, it will be blocked on this object.
  • Join the queue: add elements from the head of the queue, record putIndex (set to 0 at the end of the queue), and wake up notEmpty.
  • Out of queue operation: take out elements from the head of the queue, record takeIndex (set to 0 at the end of the queue), and wake up notFull.

How to use ArrayBlockingQueue

// Define synchronization queue
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(1000);
// Put element
System.out.println(blockingQueue.add(9));
blockingQueue.put(10);
// Extract element
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());

Source code analysis of data structure of ArrayBlockingQueue

/** The queued items */
// Data element array
final Object[] items;

/** items index for next take, poll, peek or remove */
// Index of the next element to be fetched
int takeIndex;

/** items index for next put, offer, or add */
// Index of the next element to be added
int putIndex;

/** Number of elements in the queue */
// Number of elements
int count;

/** Main lock guarding all access */
// Lock for internal use
final ReentrantLock lock;

/** Condition for waiting takes */
// Consumer condition queue
private final Condition notEmpty;

/** Condition for waiting puts */
// Producer condition queue
private final Condition notFull;

Construction method and source code analysis of ArrayBlockingQueue

/**
 * The construction method of a parameter, which only passes in the maximum length of the array.
 * Construction method of directly calling unfair mode
 */
public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}

/**
 * capacity: The length of the array and the maximum length of the queue
 * fair: Fair mode: true means fair and false means unfair
 */
public ArrayBlockingQueue(int capacity, boolean fair) {
    // If the length of the passed in array is less than 0, an exception will be thrown directly
    if (capacity <= 0)
        throw new IllegalArgumentException();
    // Initialize array
    this.items = new Object[capacity];
    // Initialization lock
    lock = new ReentrantLock(fair);
    // Initialize the consumer's condition queue
    notEmpty = lock.newCondition();
    // Initializes the producer's condition queue
    notFull =  lock.newCondition();
}

/**
 * capacity: The length of the array and the maximum length of the queue
 * fair: Fair mode: true means fair and false means unfair
 * c: You can initialize the existing list into the blocking queue.
 */
public ArrayBlockingQueue(int capacity, boolean fair,
                          Collection<? extends E> c) {
    // Call the constructor of two parameters
    this(capacity, fair);

    // Get the lock lock of the current queue
    final ReentrantLock lock = this.lock;
    // Locking operation: locking here is to prevent visibility problems caused by instruction reordering.
    lock.lock(); // Lock only for visibility, not mutual exclusion
    try {
        // Defines the temporary angle of an array element
        int i = 0;
        try {
            // Loop through each element and put it into the array of ArrayBlockingQueue
            for (E e : c) {
                // NULL pointer exception thrown when element is NULL
                checkNotNull(e);
                // Put the element into the array of ArrayBlockingQueue
                items[i++] = e;
            }
        } catch (ArrayIndexOutOfBoundsException ex) {
            // If the length of the passed in array is greater than the length of the given capacity, an exception will be thrown
            throw new IllegalArgumentException();
        }
        // Assign the number of elements to count
        count = i;
        // The elements in the array are full, and the inserted counter starts from 0
        putIndex = (i == capacity) ? 0 : i;
    } finally {
        // Release lock
        lock.unlock();
    }
}

Method of joining ArrayBlockingQueue: put (E) source code analysis

/**
 * Insert elements into ArrayBlockingQueue
 */
public void put(E e) throws InterruptedException {
    // If the element is NULL, an exception is thrown
    checkNotNull(e);
    // Get the lock lock of the current queue
    final ReentrantLock lock = this.lock;
    // Try to get the lock
    lock.lockInterruptibly();
    try {
        // When the quantity is full, the producer queue waits
        while (count == items.length)
            notFull.await();
        // Join the team
        enqueue(e);
    } finally {
        // Wake up consumer thread
        lock.unlock();
    }
}

/**
 * Queue operation
 */
private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    // Gets the current array of elements
    final Object[] items = this.items;
    // Put the current element in the added position
    items[putIndex] = x;
    // The elements in the array are full, and the inserted counter starts from 0.
    // Here, you add one at a time to point putIndex to the next position to be inserted
    if (++putIndex == items.length)
        putIndex = 0;
    // Number of elements plus one
    count++;
    // Ready to wake up consumer condition queue
    notEmpty.signal();
}

/**
 * Acquire lock operation: Lock gives priority to acquire the lock, and responds to the interrupt only after the lock is acquired successfully.
 *          lockInterruptibly Priority is given to response interrupts rather than normal or reentry acquisition of response locks.
 */
public void lockInterruptibly() throws InterruptedException {
    // Directly call the acquireinterruptible method of AQS
    sync.acquireInterruptibly(1);
}

/**
 * Acquire lock: give priority to response interrupt.
 */
public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    // If the thread is interrupted, throw an exception
    if (Thread.interrupted())
        throw new InterruptedException();
    // Lock acquisition attempt. tryAcquire is implemented in AQS in the same way as ReentrantLock
    if (!tryAcquire(arg))
        // Obtain the lock of the loop, and give priority to interrupt
        doAcquireInterruptibly(arg);
}

/**
 * Obtain the lock of the loop, and give priority to interrupt
 */
private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    // Get the current node
    final Node node = addWaiter(Node.EXCLUSIVE);
    // Define failure flag bit true
    boolean failed = true;
    try {
        for (;;) {
            // Get the previous (preceding) node of the current node. If the preceding node is null, a null pointer exception will be thrown
            final Node p = node.predecessor();
            // If the front node is the head node and the attempt to obtain the lock is successful
            if (p == head && tryAcquire(arg)) {
                // Set the current node as the head node
                setHead(node);
                // Remove the pointing of the precursor node to facilitate GC to recycle threads
                p.next = null; // help GC
                // Change failure flag bit false
                failed = false;
                // Jump out of loop
                return;
            }
            // The code executes here, indicating that the attempt to obtain the lock failed.
            // The preparation operation before blocking is successful (successful when the status is - 1)
            // Block the thread and wait for him to wake up. After waking up, return to the interrupt state of the thread!
            // The code here is in AQS, which is consistent with the implementation of ReentrantLock
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        // When the above code throws an exception, it will execute the logic here
        if (failed)
            // Cancel the logic to acquire the lock. cancelAcquire is implemented in AQS in the same way as ReentrantLock
            cancelAcquire(node);
    }
}

/**
 * Logic of conditional queue wake-up
 */
public final void signal() {
    // Not the current thread, throw an exception directly!
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // Get the header node of the condition queue
    Node first = firstWaiter;
    // The head node of the conditional queue is not null. Try to wake up the head node
    if (first != null)
        doSignal(first);
}

/**
 * Loop to wake up a single node
 */
private void doSignal(Node first) {
    do {
        // If the next node of the current node is null. This indicates that there are no other nodes after waking up.
        if ( (firstWaiter = first.nextWaiter) == null)
            // When there is no node, set the tail node to null
            lastWaiter = null;
        // Point to the current node to facilitate GC to recycle
        first.nextWaiter = null;
    // The cycle condition here is that the condition queue is transferred to the synchronization queue. transferForSignal in AQS is consistent with the implementation of CyclicBarrier
    // The transfer to synchronization queue fails and the node exists, which will cycle all the time
    // If the transfer to synchronization queue is successful or there is no node in the condition queue, jump out of the loop!
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

Out of queue method of ArrayBlockingQueue: take() source code analysis

/**
 * ArrayBlockingQueue Reduce elements in
 */
public E take() throws InterruptedException {
    // Get the lock lock of the current queue
    final ReentrantLock lock = this.lock;
    // Try to get the lock
    lock.lockInterruptibly();
    try {
        // When there is no data in the queue, the consumer waits in the queue
        while (count == 0)
            // Consumer queue waiting
            notEmpty.await();
        // Return out of line results
        return dequeue();
    } finally {
        // Wake up producer thread
        lock.unlock();
    }
}

/**
 * Out of line operation
 */
private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    // Get the element to be dequeued currently
    E x = (E) items[takeIndex];
    // Make the element to be out of the queue null for GC to recycle
    items[takeIndex] = null;
    // Out of line to the last, the next out of line counter starts from 0.
    // Here, one plus one operation is performed to point takeIndex to the next position to be out of the queue
    if (++takeIndex == items.length)
        takeIndex = 0;
    // Total number of elements minus one
    count--;
    // When the iterator is not empty
    if (itrs != null)
        // The logic here is to clear all iterators when the header node is empty!
        // Iterators need to override: iterator () definition
        itrs.elementDequeued();
    notFull.signal();
    // Returns the current element to be dequeued
    return x;
}

Concluding remarks

  • Get more pre knowledge articles of this article and new valuable articles. Let's become architects together!
  • Paying attention to the official account gives you a deep understanding of MySQL.
  • Pay attention to official account and keep continuous understanding of concurrent programming every day!
  • Pay attention to the official account, and follow the continuous and efficient understanding of spring source code.
  • This official account is not advertising!!! Update daily!!!

Topics: Java Concurrent Programming