Exploring the LinkedBlockingQueue queue of Java concurrent programming

Get into the habit of writing together! This is the 11th day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

In the previous article, we introduced the non-blocking queue ConcurrentLinedQueue implemented using the CAS algorithm. Now let's introduce the blocking queue LinkedBlockingQueue implemented using exclusive locks.

LinkedBlockingQueue is also implemented using a singly linked list. It also has two Nodes, which are used to store the head and tail nodes respectively, and an atomic variable count with an initial value of 0, which is used to record the number of queue elements. There are also two instances of ReentrantLock, 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 queue head at the same time, other threads must wait, and putLock control can only have elements at the same time. One thread can acquire the lock, add an element to the end of the queue, and other threads have to wait. In addition, notEmpty and notFull are condition variables, and they have a condition queue inside them to store the threads blocked when entering and leaving the queue. In fact, this is the producer-consumer model. The following is the creation code of the exclusive lock.

private final AtomicInteger count = new AtomicInteger();

/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
复制代码
  • When the calling thread performs take, poll and other operations on the LinkedBlockingQueue instance, it needs to acquire the takeLock lock, thereby ensuring that only one thread can operate the head node of the linked list at the same time. In addition, since the maintenance of the condition queue inside the condition variable notEmpty uses the lock state management mechanism of takeLock, the calling thread must first acquire the takeLock lock before calling the await and signal methods of notEmpty, otherwise an IllegalMonitorStateException will be thrown. A condition queue is maintained inside notEmpty. When the thread acquires the takeLock lock and calls the await method of notEmpty, the calling thread will be blocked, and then the thread will be placed in the condition queue inside notEmpty to wait until a thread calls notEmpty the signal method.

  • 在LinkedBlockingQueue实例上执行put、offer等操作时需要获取到putLock锁,从而保证同时只有一个线程可以操作链表尾节点。同样由于条件变量 notFull 内部的条件队列的维护使用的是putLock的锁状态管理机制,所以在调用 notFull 的 await 和 signal 方法前调用线程必须先获取到putLock锁,否则会抛出 IllegalMonitorStateException 异常。notFull 内部则维护着一个条件队列,当线程获取到 putLock 锁后调用notFull的await 方法时,调用线程会被阻塞,然后该线程会被放到notFull 内部的条件队列进行等待,直到有线程调用了 notFull 的 signal 方法。如下是LinkedBlockingQueue 的无参构造函数的代码。

如下是LinkedBlockingQueue的无参构造代码

public static final int MAX_VALUE = 0x7fffffff;
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}


public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalAgrumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}
复制代码

由该代码可知,默认队列容量为0x7fffffff,用户也可以自己指定容量,所以从一定程度上可以说LinkedBlockingQueue是有界阻塞队列。

offer操作

public boolean offer(E e) {
//(1)
    if (e == null) throw new NullPointerException();
    //(2)
    final AtomicInteger count = this.count;
    if (count.get() == capacity)
        return false;
        //(3)
    int c = -1;
    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;
}
复制代码

代码(2)判断如果当前队列已满则丢弃当前元素并返回false

代码(3)获取到 putLock 锁,当前线程获取到该锁后,则其他调用put和 offer操的线程将会被阻塞(阻塞的线程被放到putLock锁的AQS阻塞队列)。

代码(4)这里重新判断当前队列是否满,这是因为在执行代码(2)和获取到 putLock 锁期间可能其他线程通过 put 或者offer 操作向队列里面添加了新元素。重新判斯队列确实不满则新元素入队,并递增计数器。

代码(5)判断如果新元素入队后队列还有空闲空间,则唤醒notFull的条件队列里面因为调用了notFull的await操作(比如执行put方法而队列满了的时候)而被阻塞的一个线程,因为队列现在有空闲所以这里可以提前唤醒一个入队线程。

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.

c0 in code (7) indicates that there is at least one element in the queue when the lock is released by executing code (6), and if there is an element in the queue, the signalNotEmpty operation is performed.

Guess you like

Origin juejin.im/post/7086457613394640926