目录
阻塞队列通过Lock锁来实现:不满足条件时线程被阻塞,满足条件自动唤醒。
接口常见的两个实现类与原理:
ArrayBlockingQueue数组类型:一个lock锁+两个Condition条件+环形数组结构
LinkedBlockingQeque链表类型:两个lock锁+两个Condition条件+单向链表
ArrayBlockingQueue中Condition的判断条件依赖于三个int变量:节点数量count+下一个操作的位置标记putIndex、takeIndex。
可以把这个数组看做一个环形,最大index的下一个又回到0;
但是因为只有一把lock锁,入队与出队不能同时进行。
LinkedBlockingQeque因为入队出队可以同步进行,所以有两个不同点:
记录节点数的变量count是AtomicInteger类型的;
入队后先尝试唤起其他的入队线程,再唤醒出队线程。因为直接唤醒出队的话,出队的这段时间,入队锁就是空闲的。
BlockingQueue阻塞队列
- 当队列为空时,取元素的操作将被阻塞;
- 当队列为满时,添加元素的操作将被阻塞;
- 也就是说,在不满足条件的情况下,当前线程会 被阻塞,直到满足条件时会被自动唤起。通常使用 锁 来实现。
上篇中也列出了阻塞队列所提供的几类接口,对比于一般的队列,增加了阻塞线程的接口
- put、take:这两个方法会一直 阻塞调用线程 ,直到线程被中断或者队列状态可用;
- offer、poll:这两个方法会限时阻塞调用线程,直到线程被中断或者队列状态可用或者超时;
操作 | 抛出异常 | 返回特数值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
删除 | remove() | poll | take() | poll(time,unit) |
检查 | element() | peek | - | - |
BlockingQueue接口
public interface BlockingQueue<E> extends Queue<E> {
boolean add(E e);
boolean offer(E e);
void put(E e) throws InterruptedException;
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
E take() throws InterruptedException;
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
int remainingCapacity();
boolean remove(Object o);
public boolean contains(Object o);
int drainTo(Collection<? super E> c);
int drainTo(Collection<? super E> c, int maxElements);
}
BlockingQueue实现生产者-消费者模式
public class TestBlockingQueue {
public static void main(String[] args) throws Exception {
BlockingQueue<String> queue = new ArrayBlockingQueue(10);
Producer producer = new Producer(queue);
Consumer consumer = new Consumer(queue);
new Thread(producer).start();
new Thread(consumer).start();
}
}
class Producer implements Runnable {
protected BlockingQueue<String> queue;
public Producer(BlockingQueue queue) {
this.queue = queue;
}
public void run() {
try {
queue.put("1");
Thread.sleep(1000);
queue.put("2");
Thread.sleep(1000);
queue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Consumer implements Runnable {
protected BlockingQueue<String> queue;
public Consumer(BlockingQueue queue) {
this.queue = queue;
}
public void run() {
try {
System.out.println(queue.take());
System.out.println(System.currentTimeMillis());
System.out.println(queue.take());
System.out.println(System.currentTimeMillis());
System.out.println(queue.take());
System.out.println(System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果如下,可见Consumer线程被阻塞
1
1608090320376
2
1608090321380
3
1608090322384
【ArrayBlockingQueue】实现类
1.存储结构与构造函数
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
/** The queued items */
final Object[] items;
/** items index for next take, poll, peek or remove */
int takeIndex;
/** items index for next put, offer, or add */
int putIndex;
/** Number of elements in the queue */
int count;
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
}
- ArrayBlockingQueue是通过数组来实现元素的存储,实际存储结构为Object[] items;
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
try {
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
指定队列初始化大小:
- 创建一个指定大小的Object数组;
- 创建两个锁:notEmpty、notFull
指定队列大小、和初始集合:
- 创建一个指定大小的Object数组;
- 在循环给数组赋值之前先加锁;
- 这种形式会抛出异常的两种情况:capacity<c.size、集合c中有null元素;
所以BlockQueue中不能含有null元素。
2.【功能实现】——入队
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();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
- 加锁;
- 如果队列已满,也就是while循环中count == items.length,notFull.await()一直等待;
- 队列未满,直接入队;
3.1 putIndex记录了 下一个入队的index,将数组的此位置赋值;
3.2 putIndex+1、count+1;
3.3 如果此时队列已经满了,那么将putIndex重置为0;
3.4 notEmpty.signal()唤醒一个notEmpty的等待线程; - 释放锁;
3.【功能实现】——出队
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
- 加锁;
- 如果队列为空,也就是while循环中count == 0,notEmpty.await()一直等待;
- 队列不为空,直接出队;
3.1 takeIndex记录了下一个出队的index,返回数组的此位置元素,并将数组的index位置设为null;
3.2 takeIndex+1、count-1;
3.3 如果此时++takeIndex == items.length,表示下一个要拿出的元素是数组中最后一个位置,那么将takeIndex重置为0;
3.4 notFull.signal()唤醒一个notFull的等待线程; - 释放锁;
总结1.环形数组结构
Q:putIndex、takeIndex为什么需要重置为0?
- 可以将ArrayBlockQueue的内部结构看做一个环形,最大index的下一个位置又重新回到第一个;
- 参考:Java多线程进阶(三二)—— J.U.C之collections框架:ArrayBlockingQueue
总结2.入队出队不能同时进行
- ArrayBlockQueue内部只维护了一把全局锁,意味着生产者和消费者的活动不能同时进行。
【LinkedBlockingQeque】实现类
1.存储结构与构造函数
public class LinkedBlockingQeque<E>
extends AbstractQueue<E>
implements BlockinQqeque<E>, java.io.Serializable {
/** The capacity bound, or Integer.MAX_VALUE if none */
private final int capacity;
/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();
/**
* Head of linked list.
* Invariant: head.item == null
*/
transient Node<E> head;
/**
* Tail of linked list.
* Invariant: last.next == null
*/
private transient Node<E> last;
/** 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();
}
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
- 因为读写操作可以同时进行,两把锁的情况下并不能保证count的原子性,所以队列存储count使用的是原子类AtomicInteger;
- LinkedBlockingQueue是通过Node节点来存储元素,实际结构是一个 单向链表。
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
}
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
不指定队列的大小:
- 将队列容量设为Integer.MAX_VALUE
指定了队列的大小:
- 赋值capacity容量值;
通过已有集合赋值:
- 设置容量为Integer.MAX_VALUE
- 在循环在链表的最后加元素之前加锁;
- 这种形式会抛出异常的两种情况:capacity<c.size、集合c中有null元素;
2.【功能实现】——入队
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
- 创建一个Node节点;
- 获取putLock,加锁;
- 如果队列已满,也就是while循环中count.get() == capacity,notFull.await()一直等待;
- 队列未满,直接入队, count+1
- 如果入队以后队列未满, 唤起一个入队线程;
- 释放锁
- 判断入队之前的count是否为0,表示队列是空的,那么可能存在出队线程正在等待, 唤起一个出队线程;
3.【功能实现】——出队
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();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
- 创建一个Node节点;
- 获取takeLock,加锁;
- 如果队列为空,也就是while循环中count.get() == 0,notEmpty.await()一直等待;
- 队列不为空,直接出队, count-1
- 如果入队以后队列不为空, 唤起一个出队线程;
- 释放锁
- 判断出队之前的队列是否已满,那么可能存在入队线程正在等待, 唤起一个入队线程;
总结1.近似有界阻塞队列
- 队列在初始化时,既可以指定大小,也可以不指定。如果不指定的话,默认为Integer.MAX_VALUE,近似于无界;
总结2.入队锁出队锁分离
- 队列维护了两把锁,一把入队时获取,一把出队时获取,使得两个操作可以同时进行;
总结3.为什么入队后先唤起入队线程,再唤起出队线程?
- 根据代码的流程来看,当一个元素入队之后:
- 先判断是否还满足入队的条件,如果满足,唤起一个入队线程;
- 判断是否满足出队的条件,如果满足,唤起一个出队线程;
- 这个过程与ArrayBlockingQueue相对比:
- 入队之后直接唤起一个出队线程
- 为什么LinkedBlockingQueue入队后,不直接唤起一个出队线程,然后等出队线程结束的时候,自动的唤起下一个入队线程呢?
因为LinkedBlockingQueue的入队和出队操作是可以同时进行的,如果按照上述描述,那么出队线程执行过程这段时间,入队锁就是空闲的。