[Learning Notes - Java Collection - 16] Queue - LinkedBlockingQueue Source Analysis

Posted by jc94062 on Wed, 21 Aug 2019 19:05:41 +0200

introduce

LinkedBlockingQueue is the next blocked queue implemented as a single-chain table in a java Concurrent package. It is thread-safe. As to whether it is bounded, see the analysis below.

Source Code Analysis

Main attributes

// capacity
private final int capacity;

// Number of elements
private final AtomicInteger count = new AtomicInteger();

// Chain Headers
transient Node<E> head;

// End of Chain
private transient Node<E> last;

// take lock
private final ReentrantLock takeLock = new ReentrantLock();

// notEmpty condition
// When the queue has no elements, take locks block the notEmpty condition and wait for other threads to wake up
private final Condition notEmpty = takeLock.newCondition();

// Release lock
private final ReentrantLock putLock = new ReentrantLock();

// notFull condition
// When the queue is full, the put lock will block on notFull and wait for other threads to wake up
private final Condition notFull = putLock.newCondition();
  1. Capacity, capacity, LinkedBlockingQueue is a bounded queue
  2. head, last, chain head, chain end pointer
  3. takeLock, notEmpty, take locks and their corresponding conditions
  4. putLock, notFull, put lock and their corresponding conditions
  5. Entry and exit use two different lock controls to separate locks and improve efficiency

Internal Class

Typical single-chain table structure.

static class Node<E> {
    E item;

    Node<E> next;

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

Main construction methods

public LinkedBlockingQueue() {
    // If no capacity is passed, its capacity is initialized with the maximum int value
    this(Integer.MAX_VALUE);
}

public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    // Initialize head and last pointers to null nodes
    last = head = new Node<E>(null);
}

Entry

There are also four ways to join the team. Here we will only analyze the most important, put (E) method:

public void put(E e) throws InterruptedException {
    // null element is not allowed
    if (e == null) throw new NullPointerException();
    int c = -1;
    // Create a new node
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    // Use put lock to lock
    putLock.lockInterruptibly();
    try {
        // If the queue is full, it is blocked by the notFull condition
        // Waiting to be awakened by another thread
        while (count.get() == capacity) {
            notFull.await();
        }
        // When the queue is dissatisfied, join
        enqueue(node);
        // Queue length plus 1
        c = count.getAndIncrement();
        // If the current queue length is less than capacity
        // Wake up another thread that is blocking the notFull condition
        // Why wake up here?
        // Because there may be many threads blocking on the notFull condition
        // While fetching an element, notFull will only wake up if the queue is full before fetching it
        // Why does a full queue wake up notFull?
        // Because wake-up requires putLock, this is to reduce the number of locks
        // So, here, just check out the elements after they are released, and wake up the other threads on notFull before they are full
        // To put it plainly, this is also the cost of lock separation
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        // Release lock
        putLock.unlock();
    }
    // If the original queue length is 0, wake up the notEmpty condition immediately after adding an element
    if (c == 0)
        signalNotEmpty();
}

private void enqueue(Node<E> node) {
    // Add directly after last
    last = last.next = node;
}    

private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    // Add take lock
    takeLock.lock();
    try {
        // Wake up notEmpty condition
        notEmpty.signal();
    } finally {
        // Unlock
        takeLock.unlock();
    }
}
  1. Use putLock to lock;
  2. If the queue is full, it clogs up on the notFull condition;
  3. Otherwise join the team;
  4. If the number of elements after queuing is less than capacity, wake up other threads blocking on the notFull condition;
  5. Release the lock;
  6. If the queue length is 0 before the element is placed, the notEmpty condition is awakened;

Queue

There are also four ways to get out of the team. Here we will only analyze the most important one, the take() method:

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    // Use takeLock to lock
    takeLock.lockInterruptibly();
    try {
        // If the queue has no elements, it is blocked on the notEmpty condition
        while (count.get() == 0) {
            notEmpty.await();
        }
        // Otherwise, leave the team
        x = dequeue();
        // Get the length of the queue before leaving the queue
        c = count.getAndDecrement();
        // Wake up notEmpty if the queue length before fetching is greater than 1
        if (c > 1)
            notEmpty.signal();
    } finally {
        // Release lock
        takeLock.unlock();
    }
    // If queue length equals capacity before selection
    // Wake up notFull
    if (c == capacity)
        signalNotFull();
    return x;
}

private E dequeue() {
    // The head node itself does not store any elements
    // Delete the header here and use the next headnode as the new value
    // And leave it empty to return to its original value
    Node<E> h = head;
    Node<E> first = h.next;
    h.next = h; // help GC
    head = first;
    E x = first.item;
    first.item = null;
    return x;
}

private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        // Wake up notFull
        notFull.signal();
    } finally {
        putLock.unlock();
    }
}
  1. Use takeLock to lock;
  2. If the queue is empty, it is blocked on the notEmpty condition;
  3. Otherwise, they will leave the team.
  4. If the number of elements before queuing is greater than 1, wake up other threads blocking on the notEmpty condition;
  5. Release the lock;
  6. If the queue length is equal to capacity before taking an element, the notFull condition is awakened;

summary

  1. LinkedBlockingQueue is implemented as a single-chain table;
  2. LinkedBlockingQueue uses lock separation technology with two locks to achieve non-blocking between queues and queues.
  3. LinkedBlockingQueue is a bounded queue and defaults to the maximum int value when no capacity is passed in.

LinkedBlockingQueue versus ArrayBlockingQueue?

  1. The latter uses a lock to enter and leave the queue, which results in blocking each other and being inefficient.
  2. The former two locks are used to enter and leave the queue, which does not interfere with each other and has high efficiency.
  3. Both are bounded queues, which can cause a large number of threads to block if the queue length is equal and the queue speed cannot keep up with the queue entry speed.
  4. The former uses the maximum int value if the initialization does not pass in the initial capacity. If the queue speed cannot keep up with the queue speed, the queue will be very long and occupy a lot of memory.

Topics: Java less