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

Keywords: Java C Big Data

The previous BlockingQueue is a one-way FIFO queue, while LinkedBlockingDeque is a two-way blocking queue composed of linked lists. A two-way queue means that elements can be inserted and removed from both ends of the header and tail. It also means that LinkedBlockingDeque supports FIFO and FILO operations.

LinkedBlockingDeque is an optional capacity. You can set the capacity to prevent excessive expansion during initialization. If it is not set, the default capacity size is Integer.MAX_VALUE.

LinkedBlockingDeque

LinkedBlockingDeque inherits AbstractQueue and implements the interface BlockingDeque, and BlockingDeque inherits the interface BlockingQueue. BlockingDeque is a Queue that supports two additional operations: wait for the double ended Queue to become non empty when obtaining elements; Wait for space in the dual ended Queue to become available while storing elements. These two types of operations make it possible for the two-way operation Queue of LinkedBlockingDeque. The BlockingDeque interface provides a series of methods ending with First and Last, such as addFirst, addLast, peekFirst and peekLast.

public class LinkedBlockingDeque<E>
    extends AbstractQueue<E>
    implements BlockingDeque<E>, java.io.Serializable {

	// Header of bidirectional linked list
	transient Node<E> first;

	// Tail of bidirectional linked list
	transient Node<E> last;

	// Size, number of current nodes in the bidirectional linked list
	private transient int count;

	// Capacity, specified when creating LinkedBlockingDeque
	private final int capacity;

	final ReentrantLock lock = new ReentrantLock();

	private final Condition notEmpty = lock.newCondition();

	private final Condition notFull = lock.newCondition();

}

It can be seen from the above Lock that the underlying implementation mechanism of LinkedBlockingDeque is the same as that of LinkedBlockingQueue, which is still implemented through the mutex ReentrantLock. The two conditions of notEmpty and notFull coordinate producer and consumer issues.

Like other blockingqueues, nodes still use the internal class Node:

static final class Node<E> {
        E item;

        Node<E> prev;

        Node<E> next;

        Node(E x) {
            item = x;
        }
    }

Two way, the node must have a precursor prev and a successor next.

Basic method

The add, put, offer, take, peek and poll methods of LinkedBlockingDeque all call the XXXFirst and XXXLast methods. Therefore, only putFirst, putLast, pollFirst and pollLast are used for analysis here.

putFirst

putFirst(E e): inserts the specified element at the beginning of this double ended queue, waiting for free space if necessary.

 public void putFirst(E e) throws InterruptedException {
        // check null
        if (e == null) throw new NullPointerException();
        Node<E> node = new Node<E>(e);
        // Acquire lock
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            while (!linkFirst(node))
                // Wait on the notFull condition until awakened or interrupted
                notFull.await();
        } finally {
            // Release lock
            lock.unlock();
        }
    }

First get the lock, then call the linkFirst method to list, and finally release the lock. If the queue is full, wait on notFull. Linkfirst set the Node as the counterpart:

 private boolean linkFirst(Node<E> node) {
        // Excess capacity
        if (count >= capacity)
            return false;

        // First node
        Node<E> f = first;
        // The next of the new node points to the original first
        node.next = f;
        // Set node to new first
        first = node;

        // No tail node, set node as tail node
        if (last == null)
            last = node;
        // If there is a tail node, point the pre of the previous first to the new node
        else
            f.prev = node;
        ++count;
        // Wake up notEmpty
        notEmpty.signal();
        return true;
    }

linkFirst is mainly used to set the column header node of the node queue. It returns true if the queue is successful. If the queue is full, it returns false. The whole process is relatively simple.

putLast

putLast(E e): inserts the specified element at the end of this double ended queue, waiting for free space if necessary.

  public void putLast(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        Node<E> node = new Node<E>(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            while (!linkLast(node))
                notFull.await();
        } finally {
            lock.unlock();
        }
    }

Call linkLast to link the Node to the end of the queue:

 private boolean linkLast(Node<E> node) {
        if (count >= capacity)
            return false;
        // Tail node
        Node<E> l = last;

        // Point the precursor of the Node to the original last
        node.prev = l;

        // Set node to last
        last = node;
        // If the first node is null, set node to first
        if (first == null)
            first = node;
        else
        //If it is not null, it means that the previous last has a value, then point the next of the previous last to node
            l.next = node;
        ++count;
        notEmpty.signal();
        return true;
    }

pollFirst

pollFirst(): get and remove the first element of this double ended queue; null if this double ended queue is empty.

  public E pollFirst() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return unlinkFirst();
        } finally {
            lock.unlock();
        }
    }

Call unlinkFirst to remove the first element of the queue:

private E unlinkFirst() {
        // First node
        Node<E> f = first;

        // Empty queue, directly return null
        if (f == null)
            return null;

        // first.next
        Node<E> n = f.next;

        // Node item
        E item = f.item;

        // Remove first = = > first = first.next
        f.item = null;
        f.next = f; // help GC
        first = n;

        // Empty queue after removal, with only one node
        if (n == null)
            last = null;
        else
        // N's pre used to point to the previous first. Now n has become first, and pre points to null
            n.prev = null;
        --count;
        notFull.signal();
        return item;
    }

pollLast

pollLast(): get and remove the last element of the double ended queue; null if this double ended queue is empty.

    public E pollLast() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return unlinkLast();
        } finally {
            lock.unlock();
        }
    }

Call unliklast to remove the tail node. If the linked list is empty, null will be returned:

    private E unlinkLast() {
        // assert lock.isHeldByCurrentThread();
        Node<E> l = last;
        if (l == null)
            return null;
        Node<E> p = l.prev;
        E item = l.item;
        l.item = null;
        l.prev = l; // help GC
        last = p;
        if (p == null)
            first = null;
        else
            p.next = null;
        --count;
        notFull.signal();
        return item;
    }

Most methods of LinkedBlockingDeque are implemented through linkFirst, linkLast, unlikfirst and unliklast. Because they are two-way queues, they are all aimed at first and last operations. It is not difficult to understand the whole LinkedBlockingDeque.

After mastering the insertion and deletion of two-way queues, LinkedBlockingDeque has no difficulty. The importance of data structure!!!!
 

Posted by koenigsbote on Thu, 28 Oct 2021 11:08:38 -0700