线程安全的队列访问
主要应用场景:生产者消费者模型,是线程安全的
阻塞情况:
- 当队列满了进行入队操作
- 当队列空了的时候进行出队列操作
4种处理方式
- (Throws Exceptions) 抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
- (Special Value)返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null。
- (Blocks)一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
- (Times Out)超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。
注意: 如果是无界阻塞队列,队列不可能会出现满的情况,所以使用put或offer方法永远不会被阻塞,而且使用offer方法时,该方法永远返回true。
实现类:
Java并发包中的7个阻塞队列:
队列 | 有界性 | 锁 | 特点 |
---|---|---|---|
ArrayBlockingQueue | bounded(有界) | 加锁 | 数组 FIFO |
LinkedBlockingQueue | 指定-有界、不指定-无界 | 加锁 | 链表 FIFO |
PriorityBlockingQueue | 无界 | 加锁 | 优先级排序(默认升序) |
DelayQueue | 无界 | 加锁 | 延时获取 使用PriorityQueue实现 |
SynchronousQueue | bounded | 加锁 | 不存储元素 |
LinkedTransferQueue | 无界 | 加锁 | 链表 (传输) |
LinkedBlockingDeque | 无界 | 无锁 | 链表 双向 |
默认情况下线程采用非公平性策略访问队列
SynchronusQueue
不存储元素(没有任何容量),非常适合传递性场景,吞吐量较高。每一个put操作必须等待一个take操作,否则不能继续添加元素。
它支持公平访问队列。
//创建公平性访问 fair=true,则等待的线程会采用先进先出的顺序访问队列。
public SynchronousQueue(boolean fair) {
transferer = fair?new TransferQueue() : new TransferStack();
}
ArrayBlockingQueue
初始化时指定容量大小,有利于防止资源耗尽;一旦指定大小就不能再变。采用FIFO方式存储元素。
- 默认情况下不保证线程公平的访问队列。
- 公平访问队列是指阻塞的线程,可以按照阻塞的先后顺序访问队列,即先阻塞线程先访问队列。
- 非公平性是对先等待的线程是非公平的,当队列可用时,阻塞的线程都可以争夺访问队列的资格,有可能先阻塞的线程最后才访问 队列。
- 为了保证公平性,通常会降低吞吐量。
使用以下代码创建一个公平的阻塞队列。
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
访问者的公平性是使用可重入锁实现的,代码如下。
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);//初始化ReentrantLock重入锁,出队入队拥有这同一个锁
notEmpty = lock.newCondition;//初始化非空等待队列
notFull = lock.newCondition;//初始化非满等待队列
}
LinkedBlockingQueue
大小配置可选,如果初始化时指定了大小,那么它就是有边界的。不指定就无边界(最大整型值)。内部实现是链表,采用FIFO形式保存数据。
使用无界队列后,当核心线程都繁忙时,后续任务可以无限加入队列,线程池中线程数不会超过核心线程数。可提高线程池吞吐量,但代价是牺牲内存空间,甚至会导致内存溢出。
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);//不指定大小,无边界采用默认值,最大整型值
}
PriorityBlockingQueue
带优先级的阻塞队列(默认升序)。无边界队列,允许插入null
。必须实现Comparable接口,这样才能通过实现compareTo()方法进行排序。优先级最高的元素将始终排在队列的头部。
不会保证优先级一样的元素的排序,也不保证当前队列中除了优先级最高的元素以外的元素,随时处于正确排序的位置。
我们可以从PriorityBlockingQueue中获取一个迭代器,但这个 迭代器并不保证能按照优先级的顺序进行迭代。
public boolean add(E e) {//添加方法
return offer(e);
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);
try {
Comparator<? super E> cmp = comparator;//必须实现Comparator接口
if (cmp == null)
siftUpComparable(n, e, array);
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
LinkedTransferQueue
由链表结构组成的无界阻塞队列,采用一种预占模式:消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素。
相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。
(1)transfer方法
- 如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。
- 如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。
//试图把存放当前元素的s节点作为tail节点
Node pred = tryAppend(s, haveData);
//CPU自旋等待消费者消费元素。
//因为自旋会消耗CPU,所以自旋一定的次数后使用Thread.yield()方法来暂停当前正在执行的线程,并执行其他线程。
return awaitMatch(s, pred, e, (how == TIMED), nanos);
(2)tryTransfer方法
- tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。
- 对于带有时间限制的tryTransfer(E e,long timeout,TimeUnit unit)方法,试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在时限内消费了元素,则返回true。
(3)区别
tryTransfer方法无论消费者是否接收,方法立即返回,而transfer方法是必须等到消费者消费了才返回。
LinkedBlockingDeque
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列(可以从队列的两端插入和移出元素,减少了一半的竞争)。
相比其他的阻塞队列,LinkedBlockingDeque多了
addFirst、offerFirst、peekFirst
addLast、offerLast、peekLast 等方法
以First单词结尾的方法,表示插入、获取(peek)或移除双端队列的第一个元素。
以Last单词结尾的方法,表示插入、获取(peek)或移除双端队列的最后一个元素。
插入方法add等同于addLast
移除方法remove等效于removeFirst,但是take方法却等同于takeFirst。
使用时还是用带有First和Last后缀的方法更清楚。
在初始化LinkedBlockingDeque时可以设置容量防止其过度膨胀。另外,双向阻塞队列可以运用在“工作窃取
”模式中。
阻塞队列的实现原理
使用通知模式实现:当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。(JDK源码ArrayBlockingQueue使用了Condition来实现)
当往队列里插入一个元素时,如果队列不可用,那么阻塞生产者主要通过LockSupport.park(this)来实现。
参考:
慕课网实战·高并发探索(十三):并发容器J.U.C – 组件FutureTask、ForkJoin、BlockingQueue
阻塞队列 BlockingQueue
BlockingQueue深入解析
《java并发编程的艺术》