Java Concurrent Container Bounded Blocking Queue ArrayBlockingQueue

In this article, let's take a look at another thread-safe queue container, the ArrayBlockingQueue.

It is a bounded blocking queue implemented based on arrays. Taking JDK1.7 as a reference, the class diagram structure of ArrayBlockingQueue is as follows:

In ArrayBlockingQueue, an array is used to store queue elements, and two variables, putIndex and takeIndex, are maintained to identify the subscript positions of inserted elements and acquired elements. At the same time, a count variable is defined to count the number of queue elements. As follows:

/** The queued items */
    final Object[] items;

    /** items index for next take, poll, peek or remove */
    int takeIndex;

    /** items index for next put, offer, or add */
    int putIndex;

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

The reason why the variables defined here are not modified with the volatile keyword is that the access to these variables is carried out under the protection of locks, and there is no visibility problem.

In order to ensure thread safety, the process of dequeuing and enqueuing the queue is completed under the protection of locks. When the blocking queue is created, a ReentrantLock lock is created, and two Condition condition variable objects are created based on the lock for Synchronization of incoming and outgoing queues.

/** Main lock guarding all access */
    final ReentrantLock lock;
    /** Condition for waiting takes */
    private final Condition notEmpty;
    /** Condition for waiting puts */
    private final Condition notFull;

Put operation

The source code is as follows:

/**
     * Inserts the specified element at the tail of this queue, waiting
     * for space to become available if the queue is full.
     *
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public void put(E e) throws InterruptedException {
        checkNotNull (e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            insert(e);
        } finally {
            lock.unlock();
        }
    }
/**
     * Inserts element at current put position, advances, and signals.
     * Call only when holding lock.
     */
    private void insert(E x) {
        items [putIndex] = x;
        putIndex = inc (putIndex);
        ++count;
        notEmpty.signal();
    }

Obtain the lock before entering the queue, and then judge whether the queue is full. If so, wait for the signal() signal of the notFull condition, and then perform the insertion operation; if the queue is not full, directly insert the object into the queue. As for why lock.lockInterruptibly is used instead of lock.lock, there is a clear explanation in the comment area, thank you.

Offer operation

This operation is used to insert elements at the end of the queue. If the queue is full, it will directly return false, otherwise the object will be enqueued and return true.

/**
     * Inserts the specified element at the tail of this queue if it is
     * possible to do so immediately without exceeding the queue's capacity,
     * returning {@code true} upon success and {@code false} if this queue
     * is full.  This method is generally preferable to method {@link #add},
     * which can fail to insert an element only by throwing an exception.
     *
     * @throws NullPointerException if the specified element is null
     */
    public boolean offer(E e) {
        checkNotNull (e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                insert(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

Compared with the Put operation, this method is performed after acquiring the lock, but after acquiring the lock, if the queue is full, it returns directly to failure, and if the queue is not full, it directly joins the queue and returns success without blocking. The add method has the same function as the offer method, but the add method does not return false after the failure to join the queue, but throws an exception.

take operation

The take operation is used to get and return an element from the head of the queue, and if the queue is empty, it blocks until there is an element in the queue.

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return extract();
        } finally {
            lock.unlock();
        }
    }
private E extract() {
        final Object[] items = this.items;
        E x = this.<E>cast(items[takeIndex]);
        items[takeIndex] = null;
        takeIndex = inc(takeIndex);
        --count;
        notFull.signal();
        return x;
    }
In the take method above, when the queue is empty, the thread blocks at notEmpty.await() until it receives the signal notEmpty.signal() that the queue is not empty. After removing elements from the head of the queue, the queue is obviously not full. At this time, call notFull.signal() to notify those threads waiting to insert elements into the queue.

Poll operation

The poll operation is also used to get and remove elements from the head of the queue. If the queue is empty, it returns null directly instead of blocking waiting.

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

Peek operation

The peek operation is used to return the element at the head of the queue, but does not remove the element, or returns null if there is no element in the queue.

public E peek() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (count == 0) ? null : itemAt(takeIndex);
        } finally {
            lock.unlock();
        }
    }
/**
     * Returns item at index i.
     */
    final E itemAt(int i) {
        return this.<E>cast(items[i]);
    }

Summarize

ArrayBlockingQueue uses a global exclusive lock to realize that only one thread can enqueue or dequeue at the same time. The granularity of this lock is relatively large, which is similar to adding synchronized to the method.

Considering that the operations of the queue (enqueue, dequeue, and get elements) involve multiple implementations, some return special values, some throw exceptions, and some block, the following is a summary of these methods for your convenience .

queue operation
  special value Throw an exception block
add element offer add put
remove element poll remove take
get element peek element not support


Thank you for reading. If you are interested in Java programming, middleware, databases, and various open source frameworks, please pay attention to my blog and Toutiao (Source Code Empire). The blog and Toutiao will regularly provide some related technical articles for later. Let's discuss and learn together, thank you.

If you think the article is helpful to you, please give me a reward. A penny is not too little, and a hundred is not too much. ^_^ Thank you.



Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325984821&siteId=291194637