Learning the source code of blocking queue

1, BlockingQueue

1. BlockingQueue does not accept null elements. If NULL is written, NullPointException will be thrown

2. It is designed for production consumption queues

3. Because it indirectly inherits the Collection interface, you can remove any element in the queue through remove(x). However, except for dealing with the elements at the head and tail of the queue, operating elements in other places will affect performance. Therefore, it is possible to use the remove method occasionally and it is not recommended to use it frequently.

4. BlockingQueue is thread safe

5. Some implementation classes are limited in capacity, so be careful when inserting elements.

Example

The typical production consumption mode can ensure thread safety under multiple producers and consumers.

* class Producer implements Runnable {
 *   private final BlockingQueue queue;
 *   Producer(BlockingQueue q) { queue = q; }
 *   public void run() {
 *     try {
 *       while (true) { queue.put(produce()); }
 *     } catch (InterruptedException ex) { ... handle ...}
 *   }
 *   Object produce() { ... }
 * }
 *
 * class Consumer implements Runnable {
 *   private final BlockingQueue queue;
 *   Consumer(BlockingQueue q) { queue = q; }
 *   public void run() {
 *     try {
 *       while (true) { consume(queue.take()); }
 *     } catch (InterruptedException ex) { ... handle ...}
 *   }
 *   void consume(Object x) { ... }
 * }
 *
 * class Setup {
 *   void main() {
 *     BlockingQueue q = new SomeQueueImplementation();
 *     Producer p = new Producer(q);
 *     Consumer c1 = new Consumer(q);
 *     Consumer c2 = new Consumer(q);
 *     new Thread(p).start();
 *     new Thread(c1).start();
 *     new Thread(c2).start();
 *   }

Source code analysis

public interface BlockingQueue<E> extends Queue<E> {
//This method adds an element to the queue. If the queue is not full, it returns true. If the queue is full, it throws an IllegalStateException
    boolean add(E e);
//This method adds an element to the queue. If the queue is not full, it returns true. If the queue is full, it returns false
    boolean offer(E e);
//Add an element to the queue. If the queue is full, block until the consumer consumes the queue element to make the queue free
    void put(E e) throws InterruptedException;

 //The same logic as the put method above  
    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

//Getting an element from the queue is equivalent to consumption. However, if there is no element to consume in the queue, it will be blocked until the producer produces the element and puts it into the queue    
    E take() throws InterruptedException;

 //  Same as the take logic above
    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;
//Return remaining capacity
    int remainingCapacity();

 //Removes the specified element   
    boolean remove(Object o);

 //Whether to include the specified element  
    public boolean contains(Object o);

  //Removing all elements from the queue and adding them to the specified collection is more efficient than repeating the poll queue 
    int drainTo(Collection<? super E> c);
//The same logic as above, except that the specified number of elements in the queue are removed from the collection
    int drainTo(Collection<? super E> c, int maxElements);
}

2, BlockingDeque

1. It implements BlockingQueue, so it has all the features of the BlockingQueue interface: it does not accept a null value, is thread safe, and the capacity can be fixed or unlimited.

2. The implementation class of BlockingDeque is mainly used for a FIFO BlockingQueue.

3. Deque is double ended queue, short for double ended queue. A double ended queue is a queue in which you can insert or extract elements from either end.

4. BlockingDeque implements BlockingQueue, which has almost all the methods of BlockingQueue and its own set of methods. Its functions are more flexible and powerful than BlockingQueue.

Source code analysis

public interface BlockingDeque<E> extends BlockingQueue<E>, Deque<E> {
 //Insert an element into the head of the double ended queue. If the capacity exceeds the limit, an IllegalStateException will be thrown
    void addFirst(E e);

    //Insert an element into the end of the double ended queue. If the capacity exceeds the limit, an IllegalStateException will be thrown
    void addLast(E e);

    //This is similar to addFirst, but it has a return value
    boolean offerFirst(E e);

  //This is similar to addLast, but it has a return value
    boolean offerLast(E e);
//Insert an element into the head of the double ended queue. If the queue is full, it will wait until there is free space in the queue.
    void putFirst(E e) throws InterruptedException;

    //Insert an element to the end of the double ended queue. If the queue is full, it will wait until there is free space in the queue.
    void putLast(E e) throws InterruptedException;

    //Like putFirst, if it is full, it will wait until there is a free space
    boolean offerFirst(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

   //Like putLast, if it is full, it will wait until there is a free location
    boolean offerLast(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

   //Retrieve and remove the first element in the double ended queue. If the queue is empty, wait until there are elements
    E takeFirst() throws InterruptedException;
//Same logic as above
    E takeLast() throws InterruptedException;

   //Same as offerFirst
    E pollFirst(long timeout, TimeUnit unit)
        throws InterruptedException;

   //Same as offerLast
    E pollLast(long timeout, TimeUnit unit)
        throws InterruptedException;

   //Remove the first specified element encountered in the queue. If not, the queue remains unchanged
    boolean removeFirstOccurrence(Object o);

   
    boolean removeLastOccurrence(Object o);

    //Same as in BlockingQueue
    boolean add(E e);

   //Same as in BlockingQueue
    boolean offer(E e);

   //Insert an element to the end of the queue. If it does not meet the conditions, it will wait all the time
    void put(E e) throws InterruptedException;

   //Insert an element to the end of the queue. If it does not meet the conditions, it will wait all the time
    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

    //This method is different from poll in that it throws NoSuchElementException if the queue is empty
    E remove();

   
    E poll();

    //Retrieve and remove the header in the queue, that is, the first element of the double ended queue. If the conditions are not met, it will wait all the time
    E take() throws InterruptedException;

    //Same as above
    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

   //Retrieve but not remove the header of the queue, that is, the first element of the double ended queue. The difference between this method and peek is that if the queue is empty, this method will throw NoSuchElementException
    E element();

  
    E peek();

    //Remove the first element that matches the specified element. If there is no matching element in the queue, the queue remains unchanged
    boolean remove(Object o);

    public boolean contains(Object o);

  
    public int size();

   
    Iterator<E> iterator();

    //push an element into the stack represented by this double ended queue. This method is equivalent to addFirst(o)
    void push(E e);
}

3, The difference between BlockingQueue and BlockingDeque

1. BlockingQueue is used as message queue, and BlockingDeque implements the BlockingQueue interface, which is used as FIFO message queue.

2. If various insertion methods are used, the new element will be added to the end of the double ended queue, and the removal method will remove the element at the head of the double ended queue. Just like the insert and remove methods of the BlockingQueue interface.

4, Array blocking queue ArrayBlockingQueue

1. Bound BlockingQueue array.

2. This queue ensures that the location of elements is FIFO. The head element in the queue must exist for the longest time in the whole queue, and the tail node in the queue must live for the shortest time in the whole queue. Adding elements can only be at the end of the queue, and deleting elements can only be at the opposite end.

3. Fixed size array length. Once created, the capacity cannot be changed. Trying to put an element from a full queue or trying to take an element from an empty queue will block.

4. This queue is a fair strategy for waiting producers and consumers.
5. The queue can achieve fair and unfair access to locks in the case of multithreading. But the default is to acquire locks unfairly.

Main source code analysis

//It can be seen that this thing implements the BlockingQueue interface
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    private static final long serialVersionUID = -817911632652898426L;

//Use the Object array as a queue
    final Object[] items;
//The subscript of the header element of the queue
    int takeIndex;
//The subscript of the tail element of the queue
    int putIndex;
//Number of elements in the queue
    int count;
//ArrayBlockingQueue uses the reentrant lock to achieve thread safety and ensure FIFO fairness. However, you can also specify that the reentrant lock is unfair when constructing this object!
    final ReentrantLock lock;
//The waiting queue for the thread of the take operation
    private final Condition notEmpty;
//The wait queue for the thread of the put operation
    private final Condition notFull;
//This thing is iterators. No matter this thing, it is used for concurrent operation	
    transient Itrs itrs = null;
//The minus i of the cycle, i don't know what
    final int dec(int i) {
        return ((i == 0) ? items.length : i) - 1;
    }

    @SuppressWarnings("unchecked")
    final E itemAt(int i) {
        return (E) items[i];
    }

    private static void checkNotNull(Object v) {
        if (v == null)
            throw new NullPointerException();
    }
=============================================================================================
								Key methods					
//Join the team
    private void enqueue(E x) {
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;   //count plus one after joining the team
        notEmpty.signal();  //After joining the queue, wake up the thread of take operation waiting in the condition queue through the signal signal
    }
//Out of the team
    private E dequeue() {
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;  //Set this subscript position to null for GC convenience
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();  //After leaving the queue, wake up the thread of put operation waiting in the condition queue through the signal signal
        return x;
    }
===============================================================================================

    void removeAt(final int removeIndex) {
        final Object[] items = this.items;
        if (removeIndex == takeIndex) {
            items[takeIndex] = null;  //Convenient GC
            if (++takeIndex == items.length)
                takeIndex = 0;
            count--;
            if (itrs != null)
                itrs.elementDequeued();
        } else {
            final int putIndex = this.putIndex;
            for (int i = removeIndex;;) {
                int next = i + 1;
                if (next == items.length)
                    next = 0;
                if (next != putIndex) {
                    items[i] = items[next];
                    i = next;
                } else {
                    items[i] = null;
                    this.putIndex = i;
                    break;
                }
            }
            count--;
            if (itrs != null)
                itrs.removedAt(removeIndex);
        }
        notFull.signal();
    }
//This is the default construction method of ArrayBlockingQueue. It can be seen that the default is unfair. The purpose of this is to improve efficiency, because ensuring fairness will make threads execute in FIFO order, which is not efficient.
    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();
    }

    public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);

        final ReentrantLock lock = this.lock;
        lock.lock(); 
        try {
            int i = 0;
            try {
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }

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

================================================================================================
			Methods use reentrant locks to ensure thread safety Lock and unlock To lock and unlock
								
    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();
        }
    }

    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await(); //If the queue is full, the await method is used to block the current thread and put the generation node into the condition queue to wait.
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

    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();
        }
    }

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

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

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

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

    public int size() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }

 
    public int remainingCapacity() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return items.length - count;
        } finally {
            lock.unlock();
        }
    }

    public boolean remove(Object o) {
        if (o == null) return false;
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count > 0) {
                final int putIndex = this.putIndex;
                int i = takeIndex;
                do {
                    if (o.equals(items[i])) {
                        removeAt(i);
                        return true;
                    }
                    if (++i == items.length)
                        i = 0;
                } while (i != putIndex);
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

    public boolean contains(Object o) {
        if (o == null) return false;
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count > 0) {
                final int putIndex = this.putIndex;
                int i = takeIndex;
                do {
                    if (o.equals(items[i]))
                        return true
                    if (++i == items.length)
                        i = 0;
                } while (i != putIndex);
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

Summary:

1. ArrayBlockingQueue implements BlockingQueue, which acts as a queue through the object array.

2. FIFO can be realized, but fair lock is not used to ensure FIFO by default. Reentrantlock reentrant lock is used to ensure fairness. Lock and unlock are called to lock and unlock. Therefore, to trace back to the source, whether it is fair or not uses fair locks and unfair locks in reentrant locks.

3. Since ArrayBlockingQueue is an array implemented queue, the capacity is fixed during initialization and cannot be changed.

4. The Condition queue and signal semaphore are used to ensure the communication of threads. In other words, the thread in the Condition queue can be awakened through the signal signal signal to regain CPU resources. If the conditions are not met, call the await method to generate the node and store it in the Condition queue.

5, Conclusion

Similarly, there are chain blocking queue LinkedBlockingQueue, priority blocking queue and synchronization queue

Through the analysis of the source code, we learn how to learn the source code and how to learn, rather than learning a blocking queue!

Tags: Java

Posted on Mon, 08 Nov 2021 10:01:25 -0500 by studot