本文结合生产者消费者模式对阻塞队列的主要方法实现进行说明。
一、LinkedBlockingQueue
该队列维护的临界区由链表实现,主要成员变量如下:
capacity:临界区的上限
count:原子整型,临界区的实际大小
head:指向队首节点,控制出队过程
tail:指向队尾节点,控制入队过程
notEmpty:非空等待条件,从临界区中取资源时,若无资源则在该条件上等待
takeLock:控制每次只有一个线程从临界区中取资源
notFull:非满等待条件,往临界区中存资源时,若无空位(资源数量已经达到上限)则在该条件上等待
putLock:控制每次只有一个线程往临界区中存资源
take方法,从临界区中取资源(消费者消费资源)
主要步骤为:
1.判断临界区中是否有资源,若无则在notEmpty条件上等待,否则进入2
2.通过临界区链表的头指针head将队首资源出队,资源计数count减一
3.若take操作之前资源数等于资源上限,则说明此次操作后临界区中将有空位存在,此时唤醒等待在notFull条件上的线程
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
//当多个消费者线程因临界区中无资源,而在notEmpty上阻塞时,若某个生产者线程调用put方法往临界区中存资源,该线程将调用notEmpty.signal唤醒阻塞的消费者线程,但只会唤醒一个线程(不会调用signalAll,因为只增加了一个资源,若将阻塞消费者线程全部唤醒,大部分线程还是会因抢不到资源而继续阻塞,这样产生了无意义的上下文切换)
//消费者取资源后,若临界区中还有资源,则链式唤醒其他的消费者
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
//若取资源之前,资源数达到上限,说明此次取出后临界区中存在空位,此时唤醒一个阻塞的生产者
//为什么c < capacity时不唤醒生产者,因为c == capacity时只要唤醒了某个生产者,该生产者每次生产资源后会判断临界区中的资源数,若未达到上限,则链式唤醒其他生产者
//这样同样保证了阻塞的线程必定有机会被唤醒,另外消费者调用signalNotFull方法去唤醒生产者,需要加锁和解锁,而生产者调用notFull.signal唤醒生产者时,已经获取了锁,不必进行额外的加解锁操作
if (c == capacity)
signalNotFull();
return x;
}
put方法,往临界区中存资源(生产者生产资源),与take方法基本对称
二、ArrayBlockingQueue
该队列维护的临界区由数组实现,与LinkedBlockingQueue的主要区别为该队列只有一把锁,直接锁住整个数组,因此所有操作均为串行,首尾指针由数组和首尾索引代替,数组为“循环数组”
take方法和put方法
很简单的判断临界区资源数量,决定是否阻塞,count保证了队尾索引无论如何“前移”,均不会超越队尾指针
入队时插入资源,队尾索引”前移“
出队时置当前队首索引处资源为空,队首索引“前移”
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}