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();
- Capacity, capacity, LinkedBlockingQueue is a bounded queue
- head, last, chain head, chain end pointer
- takeLock, notEmpty, take locks and their corresponding conditions
- putLock, notFull, put lock and their corresponding conditions
- 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(); } }
- Use putLock to lock;
- If the queue is full, it clogs up on the notFull condition;
- Otherwise join the team;
- If the number of elements after queuing is less than capacity, wake up other threads blocking on the notFull condition;
- Release the lock;
- 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(); } }
- Use takeLock to lock;
- If the queue is empty, it is blocked on the notEmpty condition;
- Otherwise, they will leave the team.
- If the number of elements before queuing is greater than 1, wake up other threads blocking on the notEmpty condition;
- Release the lock;
- If the queue length is equal to capacity before taking an element, the notFull condition is awakened;
summary
- LinkedBlockingQueue is implemented as a single-chain table;
- LinkedBlockingQueue uses lock separation technology with two locks to achieve non-blocking between queues and queues.
- LinkedBlockingQueue is a bounded queue and defaults to the maximum int value when no capacity is passed in.
LinkedBlockingQueue versus ArrayBlockingQueue?
- The latter uses a lock to enter and leave the queue, which results in blocking each other and being inefficient.
- The former two locks are used to enter and leave the queue, which does not interfere with each other and has high efficiency.
- 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.
- 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.