深入理解Java并发编程之LinkedBlockingQueue队列
作者:派大大大星 时间:2022-10-02 06:35:22
前面一篇文章我们介绍了使用CAS算法实现的非阻塞队列ConcurrentLinedQueue, 下面我们来介绍使用独占锁实现的阻塞队列LinkedBlockingQueue。
LinkedBlockingQueue也是使用单向链表实现的,其也有两个Node,分别用来存放首、尾节点,并且还有一个初始值为0的原子变量count,用来记录队列元素个数。另外还有两个ReentrantLock的实例,分别用来控制元素入队和出队的原子性,其中takeLock用来控制同时只有一个线程可以从队列头获取元素,其他线程必须等待,putLock控制同时只能有一个线程可以获取锁,在队列尾部添加元素,其他线程必须等待。另外,notEmpty 和 notFull 是条件变量,它们内部都有一个条件队列用来存放进队和出队时被阻塞的线程,其实这是生产者-消费者模型。如下是独占锁的创建代码。
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();
当调用线程在LinkedBlockingQueue 实例上执行take、poll 等操作时需要获取到 takeLock 锁,从而保证同时只有一个线程可以操作链表头节点。另外由于条件变量 notEmpty 内部的条件队列的维护使用的是takeLock的锁状态管理机制,所以在调用notEmpty的await 和signal方法前调用线程必须先获取到 takeLock锁,否则会抛出IllegalMonitorStateException 异常。notEmpty内部则维护着一个条件队列,当线程获取到takeLock 锁后调用 notEmpty的await 方法时,调用线程会被阻塞,然后该线程会被放到notEmpty内部的条件队列进行等待,直到有线程调用了notEmpty的 signal 方法。
在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方法而队列满了的时候)而被阻塞的一个线程,因为队列现在有空闲所以这里可以提前唤醒一个入队线程。
代码(6)则释放获取的putLock 锁,这里要注意,锁的释放一定要在finally里面做因为即使try块抛出异常了,finally也是会被执行到。另外释放锁后其他因为调用put 操作而被阻塞的线程将会有一个获取到该锁。
代码(7)中的c0说明在执行代码(6)释放锁时队列里面至少有一个元素,队列里面有元素则执行signalNotEmpty操作.
来源:https://juejin.cn/post/7086457613394640926