Java阻塞队列
什么是阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
java中提供了七个阻塞队列,下面分别一一介绍。
ArrayBlockingQueue
介绍
一个数组实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。支持公平锁和非公平锁。
特征
- 基于数组实现,队列容量固定。
- 先进先出,队列头是最先进队的元素,队列尾是最后进队的元素。
- 存/取操作公用同一把锁(默认非公平锁
ReentrantLock
),无法实现真正意义上的存取并行。
分析
-
构造方法
由于ArrayBlockingQueue是有界队列,所以构造函数必须传入队列大小参数
//接收一个整型的参数,这个整型参数指的是队列的长度 public ArrayBlockingQueue(int capacity) { this(capacity, false); }
//boolean类型的参数是作为可重入锁的参数进行初始化,规定可重入锁是公平锁还是非公平锁,默认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(); } }
-
通过下面的构造方法可以构建出ArrayBlockingQueue对象,构造方法中会实例化一些实例变量
//保存元素的数组 final Object[] items; //出队元素下标 int takeIndex; //入队元素下标 int putIndex; //统计队列元素个数 int count; //锁对象 final ReentrantLock lock; //等待获取条件 private final Condition notEmpty; //等待存放条件 private final Condition notFull; /** * Shared state for currently active iterators, or null if there * are known not to be any. Allows queue operations to update * iterator state. */ transient Itrs itrs = null;
主要函数解析
add(E e)
-
add方法实际调用自己的
offer
,参数不能为null。 -
将 e 加到 ArrayBlockingQueue 当中,如果可以容纳返回true,否则抛出异常。
offer(E e)
- 将 e 加到 ArrayBlockingQueue 当中,如果可以容纳返回true,否则返回false。
put(E e)
- 将 e 加到 ArrayBlockingQueue 当中,如果没有空间,则调用此方法的线程会被阻断,直到有空间时继续。
poll()
- 取走 ArrayBlockingQueue 排在首位的元素并返回。
poll(long timeout, TimeUnit unit)
- 取走 ArrayBlockingQueue 排在首位的元素,若没有取到,则等待
timeout
规定的时间,取不到返回null。
take()
- 取走 ArrayBlockingQueue 排在首位的元素,如果为空,阻断等待直到有新的元素加入。
remainingCapacity()
-
返回队列剩余可用的大小。等于初始容量减去当前的size。
注意 : 不能通过该方法来判断元素是否插入成功,因为有可能另外一个元素即将插入或删除一个元素。
LinkedBlockingQueue
介绍
一个由链表实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。此队列的默认大小和长度为Integer.MAX_VALUE,所以默认创建的该队列有容量危险。
特征
- 基于链表实现,默认容量大小 Integer.MAX_VALUE。
- 存/取操作分别有独立的锁,可实现存/取并行执行。
- 基于链表,新增和移除比数组快。但是每次新增/移除都会有Node对象的新增/移除,所以存在JVM GC的性能影响的可能。
分析
-
构造方法
//无参构造,默认长度为Integer.MAX_VALUE public LinkedBlockingQueue() { this(Integer.MAX_VALUE); }
//接收一个整型的参数,这个整型参数指的是队列的长度 public LinkedBlockingQueue(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; last = head = new Node<E>(null); }
//接受一个集合类型的参数,长度默认为Integer.MAX_VALUE,集合元素不能为null 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(); } }
-
通过查看源码可以发现有两个Node,分别存储首、尾节点,并且可以看到还有一个初始值得原子变量,用来记录队列的元素个数。另外还有两个 ReentrantLock 用来控制元素入队和出队的原子性。如下
//容量大小,默认Integer.MAX_VALUE private final int capacity; //元素个数,线程安全 private final AtomicInteger count = new AtomicInteger(); //头节点 transient Node<E> head; //尾节点 private transient Node<E> last; //执行take,poll时获取该锁 private final ReentrantLock takeLock = new ReentrantLock(); //队列为空,执行出队(如take)的线程会使用该条件进行等待 private final Condition notEmpty = takeLock.newCondition(); //执行 put, offer 获取该锁 private final ReentrantLock putLock = new ReentrantLock(); //队列满了,执行入队(如put)的线程会使用该条件进行等待 private final Condition notFull = putLock.newCondition();
主要函数解析
put(E e)
- e元素为null则抛出NullPointerException异常。
- 在队列尾部插入一个元素,如果有空闲则插入完成返回,如果队列已满则阻塞当前线程,直到队列有空闲插入成功后返回。
offer(E e)
- e元素为null则抛出NullPointerException异常。
- 在队列尾部插入一个元素,如果队列有空闲则返回true,如果队列已满则丢弃当前元素然后返回false。
- 该方法是阻塞的。
poll()
- 从队列头部获取并移除一个元素,如果队列为空则返回null,该方法是不阻塞的。
poll(long timeout, TimeUnit unit)
- 取走 ArrayBlockingQueue 排在首位的元素,若没有取到,则等待
timeout
规定的时间,取不到返回null。
peek()
- 获取队列头部的元素,但不移除它。该方法是不阻塞的。
take()
- 获取队列头部的元素并从队列中移除它。
- 如果队列为空则阻塞当前线程直到队列不为空然后返回元素。
remove(Object o)
- 删除队列中指定元素的元素,有则返回true,没有则返回false。
size()
- 获取当前队列元素个数。
PriorityBlockingQueue
介绍
PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高或最低的元素。
默认使用对象的CompareTo方法提供比较规则,若需要自定义比较规则可自定义Comparators。
特征
- 基于数组实现,队列容量最大为 Integer.MAX_VALUE - 8【减8是因为数组对象头,为了避免内存溢出】
- 优先队列不允许空值,而且不支持non-compareable(不可比较对象),比如用户自定义的类。
分析
-
构造方法
//创建一个默认初始容量为11的PriorityBlockingQueue,根据元素的自然顺序对其排序 public PriorityBlockingQueue() { this(DEFAULT_INITIAL_CAPACITY, null); }
//创建一个指定初始容量的PriorityBlockingQueue,根据元素的自然顺序对其排序 public PriorityBlockingQueue(int initialCapacity) { this(initialCapacity, null); }
//创建一个指定初始容量的PriorityBlockingQueue,根据指定的比较器对元素进行排列 public PriorityBlockingQueue(int initialCapacity,Comparator<? super E> comparator) { //初始容量小于1会抛异常 if (initialCapacity < 1) throw new IllegalArgumentException(); //初始化锁 this.lock = new ReentrantLock(); //初始化条件变量 this.notEmpty = lock.newCondition(); //初始化比较器 this.comparator = comparator; //初始化数组 this.queue = new Object[initialCapacity]; }
//创建包含指定集合中的元素的PriorityBlockingQueue。 //如果指定的集合是SortedSet或PriorityQueue,则该优先级队列将按照相同的顺序进行排序。 //否则,该优先级队列将根据其元素的自然顺序进行排序。 public PriorityBlockingQueue(Collection<? extends E> c) { this.lock = new ReentrantLock(); this.notEmpty = lock.newCondition(); boolean heapify = true; // true if not known to be in heap order boolean screen = true; // true if must screen for nulls if (c instanceof SortedSet<?>) { SortedSet<? extends E> ss = (SortedSet<? extends E>) c; this.comparator = (Comparator<? super E>) ss.comparator(); heapify = false; } else if (c instanceof PriorityBlockingQueue<?>) { PriorityBlockingQueue<? extends E> pq = (PriorityBlockingQueue<? extends E>) c; this.comparator = (Comparator<? super E>) pq.comparator(); screen = false; if (pq.getClass() == PriorityBlockingQueue.class) // exact match heapify = false; } Object[] a = c.toArray(); int n = a.length; // If c.toArray incorrectly doesn't return Object[], copy it. if (a.getClass() != Object[].class) a = Arrays.copyOf(a, n, Object[].class); if (screen && (n == 1 || this.comparator != null)) { for (int i = 0; i < n; ++i) if (a[i] == null) throw new NullPointerException(); } this.queue = a; this.size = n; if (heapify) heapify(); }
-
队列中的初始化属性
//默认数组容量 private static final int DEFAULT_INITIAL_CAPACITY = 11; //分配数组的最大大小 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //存放队列元素 private transient Object[] queue; //队列中元素数量 private transient int size; //比较器,如果优先队列默认比较器,则为null private transient Comparator<? super E> comparator; //用于所有公共操作的锁 private final ReentrantLock lock; //队列为空时的阻塞条件 private final Condition notEmpty; //自旋锁,通过CAS获得 private transient volatile int allocationSpinLock; //一个普通的PriorityQueue,仅用于序列化 private PriorityQueue<E> q;
主要函数解析
offer(E e)
- offer的作用是在队列中插入一个元素,由于是无界队列,所以会一直返回 true。
poll()
- 获取队列内部堆树的根节点元素返回,并移除该元素。如果队列为空返回null。
put()
- put 内部调用的是 offer()。将指定的元素插入这个优先级队列,由于该队列是无界的,这个方法永远不会阻塞。
take()
- 获取队列内部堆树的根节点元素返回,并移除该元素。如果队列为空会阻塞,直到有线程调用 offer 方法添加成功后才会激活该线程。
size()
- 返回队列中的元素数量。该方法在返回size前加了锁,以保证在调用size()时不会有其他线程入队和出队操作。另外由于size变量没有被Volatile修饰,所以加锁也保证了在多线程下size变量的内存可见性。
使用场景
VIP排队购票 : 用户购票的时候,根据不同的等级,优先放到队伍的前面,当存在票源的时候优先分配。
import java.util.concurrent.PriorityBlockingQueue;
/**
* @author leizige
*/
class Ticket implements Comparable<Ticket> {
private Integer level;
private String ticketName;
public Ticket(Integer level, String ticketName) {
this.level = level;
this.ticketName = ticketName;
}
@Override
public int compareTo(Ticket o) {
if (this.level > o.level) {
return 1;
}
return -1;
}
@Override
public String toString() {
return this.level + "\t" + this.ticketName;
}
}
/**
* @author leizige
*/
public class PriorityBlockingQueueTest {
public static void main(String[] args) throws InterruptedException {
PriorityBlockingQueue<Ticket> ticketQueue = new PriorityBlockingQueue<>(11);
ticketQueue.add(new Ticket(0, "level-0"));
ticketQueue.add(new Ticket(4, "level-4"));
ticketQueue.add(new Ticket(2, "level-2"));
ticketQueue.add(new Ticket(1, "level-1"));
ticketQueue.add(new Ticket(3, "level-3"));
//TODO 返回该队列中元素的迭代器。迭代器不会以任何特定的顺序返回元素。
// 返回的迭代器是弱一致的返回:一个遍历此队列中的元素的迭代器
// Iterator<Ticket> iterator = ticketQueue.iterator();
// while (iterator.hasNext()) {
// System.err.println(iterator.next().toString());
// }
do {
if (!ticketQueue.isEmpty())
System.err.println(ticketQueue.take().toString());
} while (!ticketQueue.isEmpty());
}
}
DelayBlockingQueue
介绍
DelayQueue并发队列是一个无界阻塞延迟队列,队列中的每个元素都有个过期时间,当从队列获取元素时,只有过期元素才会出队列。队列头元素是最快要过期的元素。
特征
- 不允许空元素。
- 存储元素必须实现Delayed接口(Delayed接口继承了Comparable接口)。
- 内部使用 PriorityQueue 存储数据。
分析
由于基于优先队列实现,但是它比较的是时间,可以根据需要来倒序或者正序排列。
-
构造方法
//初始化一个空的DelayQueue public DelayQueue() {}
//创建一个DelayQueue,其中包含传入得集合元素 public DelayQueue(Collection<? extends E> c) { this.addAll(c); }
-
队列中的每个元素都要实现Delayed接口,由于每个元素都有一个过期时间,所以要实现获知当前元素剩下的过期时间的接口。由于队列内部使用优先队列来实现,所以要实现元素之间互相比较的接口。
public interface Delayed extends Comparable<Delayed> { /** * Returns the remaining delay associated with this object, in the * given time unit. * * @param unit the time unit * @return the remaining delay; zero or negative values indicate * that the delay has already elapsed */ long getDelay(TimeUnit unit); }
主要函数解析
offer(E e)
- 插入元素到队列,如果元素为null则抛出NullPointerException异常,由于是无界队列,会一直返回true。插入的元素必须实现 Delayed接口。
take()
- 获取并移除队列中延迟过期的元素,如果队列中没有过期元素则等待。
poll()
- 获取并移除队头过期元素,如果没有过期元素返回null。
size()
- 获取队列元素个数,包含过期和未过期的。
使用场景
- 订单业务 :下单之后如果三十分钟之内没有付款就自动取消订单。
- 短信通知 :下单成功六十秒后给用户发送短信通知。
简单实现
package queue;
import javax.validation.constraints.NotNull;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* @author leizige
*/
public class Work implements Delayed {
/**
* 数据
*/
private String msg;
/**
* 延迟时间
*/
private Long delay;
/**
* 到期时间
*/
private Long expire;
/***
* 创建时间
*/
private Long now;
private Work() {
}
public Work(String msg, Long delay) {
this.msg = msg;
this.delay = delay;
/* 到期时间 = 当前时间 + 延迟时间 */
expire = System.currentTimeMillis() + delay;
now = System.currentTimeMillis();
}
/**
* 需要实现的接口,获得延迟时间 过期时间 - 当前时间
*/
@Override
public long getDelay(@NotNull TimeUnit unit) {
return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
/**
* 延迟队列内部排序,当前时间的延迟时间 - 比较对象的延迟时间
*/
@Override
public int compareTo(@NotNull Delayed o) {
return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
}
@Override
public String toString() {
return "work{" + "delay=" + delay +
", expire=" + expire +
", msg='" + msg + '\'' +
", now=" + now +
'}';
}
}
package queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.DelayQueue;
/**
* @author leizige
*/
public class DelayQueueTest {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Work> delayQueue = new DelayQueue<>();
delayQueue.add(new Work("obj1", 1000L));
delayQueue.add(new Work("obj3", 3000L));
delayQueue.add(new Work("obj8", 8000L));
delayQueue.add(new Work("obj2", 2000L));
do {
if (!delayQueue.isEmpty())
System.err.println(delayQueue.take().toString());
} while (!delayQueue.isEmpty());
}
}
SynchronouseQueue
介绍
SynchronouseQueue 是一个不存储元素的阻塞队列。每一个put操作必须等待tack操作,否则不能添加元素。支持公平锁和非公平锁,默认情况下使用非公平性策略访问队列。
SynchronousQueue的一个使用场景是在线程池中,Executors.newCachedThreadPool()中就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲60s后会被回收。
分析
SynchronousQueue 可以看做一个传递者,负责把生产者线程处理的数据直接传递给消费者处理的线程,队列本身不存储任何元素,非常适合传递性的场景。并且 SynchronousQueue 的吞吐量高于 ArrayBlockingQueue 和 LinkedBlockingQueue。
-
构造方法
//创建不公平访问策略的SynchronousQueue public SynchronousQueue() { this(false); }
//创建公平性访问的SynchronousQueue,如果fair==true,则等待的线程会以先进先出的顺序访问队列 public SynchronousQueue(boolean fair) { transferer = fair ? new TransferQueue<E>() : new TransferStack<E>(); }
-
SynchronousQueue 使用了内部类 Transfer 来转移数据,
/** * Shared internal API for dual stacks and queues. */ abstract static class Transferer<E> { /** * Performs a put or take. * * @param 如果e为null,则说明这是一个消费者线程,比如take操作 * 如果e不为null,那么就是生产者线程,这个数据就是要交付的数据,比如put * @param timed if this operation should timeout * @param nanos the timeout, in nanoseconds * @return if non-null, the item provided or received; if null, * the operation failed due to timeout or interrupt -- * the caller can distinguish which of these occurred * by checking Thread.interrupted. */ abstract E transfer(E e, boolean timed, long nanos); }
SynchronousQueue 采用队列 TransferQueue 来实现公平性策略,采用堆栈 TransferStack 来实现非公平性策略。
-
TransferQueue ,TransferQueue 使用了 QNode 来作为队列节点
/** Node class for TransferQueue. */ static final class QNode { //队列中下一个节点 volatile QNode next; // next node in queue //数据项 volatile Object item; // CAS'ed to or from null //等待线程 volatile Thread waiter; // to control park/unpark //是否为数据标识 final boolean isData; }
TransferQueue 中主要有一下三个 QNode 对象
//队列头 transient volatile QNode head; //队列尾部 transient volatile QNode tail; //指向一个已取消的节点,该节点可能未从队列中断开连接 transient volatile QNode cleanMe;
TransferQueue 构造方法
TransferQueue() { QNode h = new QNode(null, false); // initialize to dummy node. head = h; tail = h; }
-
TransferStack ,TransferStack 使用了 SNode 来作为队列节点
/** Node class for TransferStacks. */ static final class SNode { //队列中下一个节点 volatile SNode next; // next node in stack //匹配的节点 volatile SNode match; // the node matched to this //等待线程 volatile Thread waiter; // to control park/unpark //数据项 Object item; // data; or null for REQUESTs int mode; }
节点主要有以下三个状态
//节点表示未满足的消费者 static final int REQUEST = 0; //节点表示未满足的生产者 static final int DATA = 1; //节点正在执行另一个未完成的数据或请求 static final int FULFILLING = 2;
-
SynchronousQueue中的 put 、take都是依赖 TransferQueue或TransferTake的transfer方法实现的
//将指定的元素添加到此队列中,并在必要时等待另一个线程接收它 public void put(E e) throws InterruptedException { //不能为空 if (e == null) throw new NullPointerException(); //调用transfer方法 if (transferer.transfer(e, false, 0) == null) { Thread.interrupted(); throw new InterruptedException(); } }
public E take() throws InterruptedException { //调用transfer方法 E e = transferer.transfer(null, false, 0); if (e != null) return e; Thread.interrupted(); throw new InterruptedException(); }
使用场景
插入数据的线程和获取数据的线程,交替执行
package queue;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
/**
* @author leizige
*/
public class SynchronousQueueExample {
static class SynchronousQueueProducer implements Runnable {
private BlockingQueue<String> blockingQueue;
public SynchronousQueueProducer(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
while (true) {
try {
String data = UUID.randomUUID().toString();
System.out.println("Put:" + data);
blockingQueue.put(data);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class SynchronousQueueConsumer implements Runnable {
private BlockingQueue<String> blockingQueue;
public SynchronousQueueConsumer(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
while (true) {
try {
String data = blockingQueue.take();
System.out.println(Thread.currentThread().getName()
+ " take(): " + data);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
final BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
SynchronousQueueProducer queueProducer = new SynchronousQueueProducer(blockingQueue);
new Thread(queueProducer).start();
SynchronousQueueConsumer queueConsumer_1 = new SynchronousQueueConsumer(blockingQueue);
new Thread(queueConsumer_1).start();
SynchronousQueueConsumer queueConsumer_2 = new SynchronousQueueConsumer(blockingQueue);
new Thread(queueConsumer_2).start();
}
}
应用场景
Executors.newCachedThreadPool()
/**
* Creates a thread pool that creates new threads as needed, but
* will reuse previously constructed threads when they are
* available. These pools will typically improve the performance
* of programs that execute many short-lived asynchronous tasks.
* Calls to {@code execute} will reuse previously constructed
* threads if available. If no existing thread is available, a new
* thread will be created and added to the pool. Threads that have
* not been used for sixty seconds are terminated and removed from
* the cache. Thus, a pool that remains idle for long enough will
* not consume any resources. Note that pools with similar
* properties but different details (for example, timeout parameters)
* may be created using {@link ThreadPoolExecutor} constructors.
*
* @return the newly created thread pool
*/
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
LinkedTransferQueue
介绍
一个由链表结构组成的无界阻塞队列。相对于其他队列,LinkedTransferQueue队列多了transfer和tryTransfer方法。
......
LinkedBlockingDeque
介绍
一个由链表结构组成的双向阻塞队列。队列头部和尾部可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。
......