本文转自:Java中的阻塞队列(BlockingQueue)
阻塞队列(BlockingQueue)是 Java 5 并发新特性中的内容,阻塞队列的接口是 java.util.concurrent.BlockingQueue,它提供了两个附加操作:当队列中为空时,从队列中获取元素的操作将被阻塞;当队列满时,向队列中添加元素的操作将被阻塞。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器。
阻塞队列提供了四种操作方法:
JDK7提供了7个阻塞队列。分别是
下面分别简单介绍一下:
- transfer()方法:如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法),transfer()方法可以把生产者传入的元素立刻传输给消费者;如果没有消费者在等待接收元素,transfer()方法会将元素存放到队列的tail节点,并等到该元素被消费者消费了才返回。
- tryTransfer()方法:该方法是用来试探生产者传入的元素是否能直接传给消费者,如果没有消费者等待接收元素,则返回false。与transfer()方法的区别:tryTransfer()方法是立即返回(无论消费者是否接收),transfer()方法是必须等到消费者消费了才返回。对于带有时间限制的tryTransfer(E e, long timeout, TimeUnit unit)方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间之后再返回,如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true。
Java中线程安全的内置队列还有两个:ConcurrentLinkedQueue和LinkedTransferQueue,它们使用了CAS这种无锁的方式来实现了线程安全的队列。无锁的方式性能好,但是队列是无界的,用在生产系统中,生产者生产速度过快,可能导致内存溢出。有界的阻塞队列ArrayBlockingQueue和LinkedBlockingQueue,为了减少Java的垃圾回收对系统性能的影响,会尽量选择array/heap格式的数据结构。这样的话就只剩下ArrayBlockingQueue。(先埋个坑在这儿,近来接触到了disruptor,感觉妙不可言。disruptor)
构造方法:
ArrayBlockingQueue(int capacity);
ArrayBlockingQueue(int capacity, boolean fair);
ArrayBlockingQueue(int capacity, boolean fair, Collection extends E> c)
ArrayBlockingQueue提供了三种构造方法,参数含义如下:
插入元素:
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();
}
}
从源码可以看出,生产者首先获得锁lock,然后判断队列是否已经满了,如果满了,则等待,直到被唤醒,然后调用enqueue插入元素。
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();
}
以上是enqueue的实现,实现的操作是插入元素到一个环形数组,然后唤醒notEmpty上阻塞的线程。
获取元素:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
从源码可以看出,消费者首先获得锁,然后判断队列是否为空,为空,则等待,直到被唤醒,然后调用dequeue获取元素。
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;
}
以上是dequeue的实现,获取环形数组当前takeIndex的元素,并及时将当前元素置为null,设置下一次takeIndex的值takeIndex++,然后唤醒notFull上阻塞的线程。
还有其他方法offer(E e)、poll()、add(E e)、remove()、 offer(E e, long timeout, TimeUnit unit)等的实现,因为常用take和put,这些方法就不一一赘述了。
构造方法:
public LinkedBlockingQueue()
public LinkedBlockingQueue(int capacity)
public LinkedBlockingQueue(Collection extends E> c)
LinkedBlockingQueue 的默认的容量是Integer.MAX_VALUE,这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。使用时尽量使用上面的第二个构造函数指定初始容量。
成员变量
/** The capacity bound, or Integer.MAX_VALUE if none */
private final int capacity;
/** 队列中当前元素数目 */
private final AtomicInteger count = new AtomicInteger();
/** head 节点 */
transient Node head;
/** tail 节点* Invariant: last.next == null */
private transient Node 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();
LinkedBlockingQueue 中的元素添加时使用 last 节点添加到队列结尾,使用 head 节点从队列前面移除节点,元素读取按照先进先出的顺序。和 ArrayBlockingQueue在put,take操作使用了同一个锁有区别的是,LinkedBlockingQueue使用了不同的锁,put操作和take操作可同时进行,以此来提高整个队列的并发性能。
ArrayBlockingQueue 与 LinkedBlockingQueue 两者对比
插入元素
/**
* Inserts the specified element at the tail of this queue, waiting if
* necessary for space to become available.
*/
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
// 如果容量已满,则等待,直到有可用容量
while (count.get() == capacity) {
notFull.await();
}
// 插入元素
enqueue(node);
c = count.getAndIncrement();
//如果元素插入之后,总的元素数目小于容量,通知 容量未满
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
// 如果第一个元素插入成功,则通知消费者已经有元素
if (c == 0)
signalNotEmpty();
}
/**
* Signals a waiting take. Called only from put/offer (which do not
* otherwise ordinarily lock takeLock.)
*/
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
private void enqueue(Node node) {
// 将新的节点添加到队列末尾
last = last.next = node;
}
读取元素:
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();
}
// 如果移除元素后,元素数目刚好比容量数小1,则通知等待的写入锁
if (c == capacity)
signalNotFull();
return x;
}
private E dequeue() {
// 使 head 节点替换第一个节点
Node h = head;
Node first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
drainTo():
一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数), 通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。
public int drainTo(Collection super E> c) {
return drainTo(c, Integer.MAX_VALUE);
}
public int drainTo(Collection super E> c, int maxElements) {
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
if (maxElements <= 0)
return 0;
boolean signalNotFull = false;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
int n = Math.min(maxElements, count.get());
// count.get provides visibility to first n Nodes
Node h = head;
int i = 0;
try {
while (i < n) {
Node p = h.next;
c.add(p.item);
p.item = null;
h.next = h;
h = p;
++i;
}
return n;
} finally {
// Restore invariants even if c.add() threw
if (i > 0) {
// assert h.item == null;
head = h;
signalNotFull = (count.getAndAdd(-i) == capacity);
}
}
} finally {
takeLock.unlock();
if (signalNotFull)
signalNotFull();
}
}
使用阻塞队列实现生产者-消费者模式:
/**
* Created by noly on 2017/5/19.
*/
public class BlockingQueueTest {
public static void main (String[] args) {
ArrayBlockingQueue queue = new ArrayBlockingQueue(10);
Consumer consumer = new Consumer(queue);
Producer producer = new Producer(queue);
producer.start();
consumer.start();
}
}
class Consumer extends Thread {
private ArrayBlockingQueue queue;
public Consumer(ArrayBlockingQueue queue){
this.queue = queue;
}
@Override
public void run() {
while(true) {
try {
Integer i = queue.take();
System.out.println("消费者从队列取出元素:" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread {
private ArrayBlockingQueue queue;
public Producer(ArrayBlockingQueue queue){
this.queue = queue;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
queue.put(i);
System.out.println("生产者向队列插入元素:" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
如果不使用阻塞队列,使用Object.wait()和Object.notify()、非阻塞队列实现生产者-消费者模式,考虑线程间的通讯,会非常麻烦。
实体类
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
// 必须实现Delayed接口
public class Student implements Delayed {
private String name;
private long submitTime; // 交卷时间(开始考试时间默认为实例初始化时间)
private long workTime; // 考试时长
public Student(String name, long workTime) {
this.name = name;
this.workTime = workTime;
this.submitTime = TimeUnit.NANOSECONDS.convert(workTime, TimeUnit.MILLISECONDS) + System.nanoTime();
System.out.println(this.name + " 交卷,用时" + workTime);
}
public String getName() {
return this.name + " 交卷,用时" + workTime;
}
// 重写 compareTo 方法,用于指定元素的顺序
@Override
public int compareTo(Delayed o) {
Student that = (Student) o;
return submitTime > that.submitTime ? 1 : (submitTime < that.submitTime ? -1 : 0);
}
// 必须实现该方法
@Override
public long getDelay(TimeUnit unit) {
//返回一个延迟时间 (这里是 考试所用时间)
return unit.convert(submitTime - System.nanoTime(), TimeUnit.NANOSECONDS);
}
}
测试
import java.util.concurrent.DelayQueue;
public class DelayQueueTest {
public static void main(String[] args) throws InterruptedException {
// 新建一个等待队列
final DelayQueue bq = new DelayQueue();
Student stu;
for (int i = 0; i < 5; i++) {
stu = new Student("学生" + i, Math.round(Math.random() * 1000 + i));
bq.put(stu); // 将数据存到队列里!
}
long start = System.currentTimeMillis();
System.out.println(bq.take().getName());
System.out.println(System.currentTimeMillis() - start);
}
}
执行结果:
学生0 交卷,用时602
学生1 交卷,用时174
学生2 交卷,用时499
学生3 交卷,用时580
学生4 交卷,用时464
学生1 交卷,用时174
176
聊聊并发(七)——Java中的阻塞队列
阻塞队列和ArrayBlockingQueue源码解析
高性能队列——Disruptor