2
头图

Blocking queue LinkedBlockingQueue

introduce

The previous article introduced the non-blocking queue ConcurrentLinkedQueue implemented CAS algorithm. This article introduces the blocking queue LinkedBlockingQueue exclusive lock.

类图

In this class diagram, you can see that LinkedBlockingQueue is also implemented using a singly linked list, which includes head Node and last Node, which are used to store head and tail nodes; and there is also a atomic variable count with an initial value of 0, which is used to record the number of queue elements. In addition, it also contains two ReentrantLock instances, which are used to control the atomicity of element entry and dequeue respectively. TakeLock is used to control that only one thread can obtain elements from the head of the queue at the same time, and putLock is used to control only one thread at the same time. Elements can be added from the tail of the queue.

notEmpty and notFull are condition variables, and they have a condition queue inside them to store the threads blocked when entering and exiting the queue.

The constructor of LinkedBlockingQueue is as follows:

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

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

It can be seen that by default, the LinkedBlockingQueue queue capacity is the maximum value of int. Of course, we can also directly specify the capacity size, so to a certain extent, it can be explained that LinkedBlockingQueue is bounded blocking queue .

offer action

Inserts an element to the end of the queue. If the queue is free, the insertion succeeds and returns true. If the queue is full, discards the current element and returns false. Throws an exception if the inserted element is null. And the method is non-blocking .

public boolean offer(E e) {
    //(1)元素为空则抛出异常
    if (e == null) throw new NullPointerException();
     //(2)如果队列已满则丢弃元素
    final AtomicInteger count = this.count;
    if (count.get() == capacity)
        return false;
    int c = -1;
    //(3)构建新节点,然后获取putLock独占锁
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        //(4)如果队列不满则进入队列,并增加计数
        if (count.get() < capacity) {
            enqueue(node);
            c = count.getAndIncrement();
            //(5)
            if (c + 1 < capacity)
                notFull.signal();
        }
    } finally {
        //(6)释放锁
        putLock.unlock();
    }
      //(7)
    if (c == 0)
        signalNotEmpty();
      //(8)
    return c >= 0;
}

Code (1) first determines whether the enqueued element is empty, and throws an exception if it is empty.

Code (2) judges whether the queue is full, discards it and returns false if it is full.

Code (3) builds a new Node node, and then acquires the putLock lock. When the lock is acquired, other threads calling the put or offer method will be blocked and put into the AQS blocking queue of putLock.

Code (4) judges whether the queue is full, why judge again? Because there may be other threads adding new elements through put or offer between the execution of code (2) and the acquisition of the lock, it is judged again whether the queue is full.

Code (5) judges that if there is still space left after the new element is added to the queue, wake up a thread that is blocked in the notFull condition queue, (wake up the thread that calls the await operation of notFull, such as when the put method is executed and the queue is full ).

Code (6) releases the acquired putLock lock. It should be noted here that the release of the lock must be done in finally, because even if the try block throws an exception, finally will be executed. In addition, after the lock is released, other threads blocked by calling the put operation will have one to acquire the lock.

Code (7) c == 0 indicates that the queue has at least one element when executing code (6) to release the lock, and the signalNotEmpty method is executed if there are elements in the queue.

private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}

The function of this method is to activate a thread in the condition queue of notEmpty that is blocked due to calling the await method of notEmpty (for example, when the take method is called and the queue is empty). Lock.

summarizes : The offer method uses the putLock lock to ensure the added atomicity. In addition, it should be noted that when calling the condition variable, the corresponding lock needs to be acquired first, and the queue only operates the tail node of the linked list.

put operation

This method inserts an element to the end of the queue. If the queue is free, it will return directly after the insertion is successful. If the queue is full, block the current thread until the queue is free and the insertion will return successfully. If the interrupt flag is set by another thread while blocking, the blocked thread will throw an exception and return. if the passed element is null.

public void put(E e) throws InterruptedException {
    //(1)插入元素为空抛出异常
    if (e == null) throw new NullPointerException();
    int c = -1;
      //(2)构建新节点,并获取独占锁putLock
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        //(3)如果队列满则等待
        while (count.get() == capacity) {
            notFull.await();
        }
        //(4)插入队列并递增计数
        enqueue(node);
        c = count.getAndIncrement();
         //(5)
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        //(6)
        putLock.unlock();
    }
    //(7)
    if (c == 0)
        signalNotEmpty();
}

putLock.lockInterruptibly() is used to acquire the exclusive lock in code (2), which can be interrupted compared to the method of acquiring the exclusive lock in the offer method. Specifically, when the current thread acquires the lock, if the interrupt flag is set by another thread, the current thread will throw an InterruptedException, so the put operation can be interrupted in the process of acquiring the lock.

Code (3) Judging that the current queue is full, call the notFull.await() method to put the current thread into the notFull conditional queue, and then the current queue will release the putLock lock. Since the putLock lock is released, other threads have the opportunity to acquire the Lock.

Using while instead of if is to prevent spurious wakeup problems

poll operation

Gets and removes an element from the head of the queue, or returns null if the queue is empty, this method is a non-blocking method.

public E poll() {
    final AtomicInteger count = this.count;
    //(1)队列为空则返回null
    if (count.get() == 0)
        return null;
    //(2)获取独占锁
    E x = null;
    int c = -1;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        //(3)队列不为空则出队并且递减计数
        if (count.get() > 0) {
            x = dequeue();
            c = count.getAndDecrement();
            //(4)
            if (c > 1)
                notEmpty.signal();
        }
    } finally {
        //(5)
        takeLock.unlock();
    }
    //(6)
    if (c == capacity)
        signalNotFull();
    //(7)
    return x;
}
private E dequeue() {
    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;
}

First of all, the code (1) is very simple. First, judge whether the current queue is empty, and return null directly if it is empty.

Code (2) acquires the exclusive lock takeLock. After the current thread acquires the lock, other threads will be blocked when calling the poll or take method.

Code (3) judges that if the current queue is not empty, the dequeue operation is performed, and then the counter is decremented.

Although the determination of whether the queue is empty and the acquisition of queue elements in code (3) are not atomic, the count value will only be decremented in the place where the poll, take or remove operations are performed, but these three methods all need to acquire the takeLock lock. operation, while the current thread has acquired the takeLock lock, so other threads have no chance to decrement the count value in the current situation, so it does not seem atomic, but they are thread safe.

Code (4) judges that if c > 1 it means that the queue is not empty after the current thread removes an element in the queue (c is the number of elements in the queue before deleting the element), then it can be activated at this time because the call to the take method is blocked until A thread in the condition queue of notEmpty.

Code (6) shows that the current queue is full before the current thread removes the head element, and there is at least one free position in the current queue after removing the head element, then signalNotFull can be called at this time to activate because the put method is called and blocked to notFull A thread in the condition queue of , the code for signalNotFull is as follows.

private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        notFull.signal();
    } finally {
        putLock.unlock();
    }
}

peek operation

Gets the element at the head of the queue without removing it from the queue, or returns null if the queue is empty. The method is the non-blocking method.

public E peek() {
    //(1)
    if (count.get() == 0)
        return null;
     //(2)
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        Node<E> first = head.next;
        //(3)
        if (first == null)
            return null;
        else
            //(4)
            return first.item;
    } finally {
        takeLock.unlock();
    }
}

The peek method is not very complicated. It should be noted that first == null needs to be judged here, and first.item cannot be returned directly, because code (1) and code (2) are not atomic, it is very likely that the queue is not determined in code (1). After it is empty, other threads perform poll or take operations before acquiring the lock, causing the queue to be empty, and then returning fist.item directly will result in a null pointer exception.

take operation

This method gets the current queue head element and removes it from the queue. If the queue is empty, it blocks the current thread until the queue is not empty and then gets and returns the element. If the interrupt flag is set by another thread while blocking, the thread is blocked will throw an exception.

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    //(1)获取锁
    takeLock.lockInterruptibly();
    try {
           //(2)队列为空则阻塞挂起
        while (count.get() == 0) {
            notEmpty.await();
        }
        //(3)出队并递减计数
        x = dequeue();
        c = count.getAndDecrement();
        //(4)
        if (c > 1)
            notEmpty.signal();
    } finally {
        //(5)
        takeLock.unlock();
    }
    //(6)
    if (c == capacity)
        signalNotFull();
    return x;
}

In code (1), the current thread acquires an exclusive lock, and other threads calling take or poll operations will be blocked and suspended.

Code (2) judges that if the queue is empty, blocks and suspends the current thread, and puts the current thread into the condition queue of notEmpty.

Code (3) dequeues and counts down.

Code (4) judges that if c > 1 indicates that the current queue is not empty, then wake up a thread in the condition queue of notEmpty that was blocked due to calling the take operation.

Code (5) releases the lock.

Code (6) judges that if c == capacity indicates that the current queue has at least one free position, then activate a thread in the condition queue of notFull that was blocked due to calling the put operation.

remove operation

Delete the specified element in the queue, delete and return true if there is one, and return false if there is no.

public boolean remove(Object o) {
    if (o == null) return false;
    //(1)获取putLock 和 takeLock
    fullyLock();
    try {
        //(2)遍历寻找要删除的元素
        for (Node<E> trail = head, p = trail.next;
             p != null;
             trail = p, p = p.next) {
            //(3)
            if (o.equals(p.item)) {
                unlink(p, trail);
                return true;
            }
        }
        //(4)
        return false;
    } finally {
        //(5)
        fullyUnlock();
    }
}
void fullyLock() {
    putLock.lock();
    takeLock.lock();
}

First, the code (1) acquires the double lock, and then the enqueue and dequeue operations of other threads are suspended.

Code (2) traverses the queue to find the element to be deleted, returns false if not found, finds and executes the unlink method, let's take a look at this method.

void unlink(Node<E> p, Node<E> trail) {
        p.item = null;
        trail.next = p.next;
        if (last == p)
            last = trail;
        //如果当前队列满,再删除后也要唤醒等待的线程
        if (count.getAndDecrement() == capacity)
            notFull.signal();
}

trail is the precursor node to delete the element. After the element is deleted, if there is free space in the current queue, it will wake up a thread in the notFull condition queue that was blocked because the put method was called.

Code (5) calls fullyUnlock to release the double lock.

void fullyUnlock() {
    takeLock.unlock();
    putLock.unlock();
}

Summary : Since the remove method adds two locks before deleting the specified element, it is thread-safe in the process of traversing the queue to find the specified element, and all other threads calling the queue entry and dequeue operations will be blocked at this time. . Note that acquires multiple resource locks is opposite to the order in which they are released .

size operation

Get the current number of queue elements.

public int size() {
    return count.get();
}

Since the count of dequeue and enqueue operations are locked, the result is accurate compared to the size method of ConcurrentLinkedQueue.

Summarize

总结队列图

The interior of LinkedBlockingQueue is implemented through a singly linked list, using head and tail nodes to perform enqueue and dequeue operations, that is, enqueue operations are performed on the tail node, and dequeue operations are performed on the head node.

The operations on the head and tail nodes use separate exclusive locks to ensure atomicity, so the dequeue and enqueue operations can be performed at the same time. In addition, the exclusive locks of the head and tail nodes are equipped with a conditional queue to store blocked threads, and a production and consumption model is realized by combining the enqueue and dequeue operations.


神秘杰克
765 声望383 粉丝

Be a good developer.