7 种阻塞队列相关整理
1. 简介
- 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
- 两个附加的操作。
- 支持阻塞的 插入 方法,在队列为空时,获取元素的线程会等待队列变为非空。
- 支持阻塞的 移除 方法,当队列满时,存储元素的线程会等待队列可用。
- 阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。
- 阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
- BlockingQueue 是一个继承自 Queue 的接口,在 Queue 的队列基础上增加了阻塞操作。
Queue 接口
- 入队、出队和检索均有两种实现,区别在于其中一种会在方法执行失败时抛出异常,而另一种会在失败时返回值。
- 以入队为例,
add()
方法在队列已满时将会抛出异常,而offer()
在队列已满时会返回 false。
- 以入队为例,
入队 | 出队 | 检索 | 处理方式 |
---|---|---|---|
add() | remove() | element() | 在执行方法失败时不返回值,抛出异常。 |
offer() | poll() | peek() | 在执行方法时,给出返回值,比如 false、null。 |
BlockingQueue 接口
- BlockingQueue 在 Queue 接口的基础上对入队和出队两个操作分别又增加了阻塞方法。
- 设置了时间的入队和出队操作,在时间到了之后如果还未执行成功,那么返回 false 和 null。
put()
和take()
方法会一直阻塞。
阻塞入队 | 阻塞出队 | 定时入队 | 定时出队 |
---|---|---|---|
put(E e) | E take() | offer(E e,long timeout,TimeUnit unit) | E poll(long timeout,TimeUnit unit) |
2 Java 中的阻塞队列
- JDK 中提供了 7 个阻塞队列。
阻塞队列 | 说明 |
---|---|
ArrayBlockingQueue | 一个由数组结构组成的有界阻塞队列。 |
LinkedBlockingQueue | 一个由链表结构组成的有界阻塞队列。 |
PriorityBlockingQueue | 一个支持优先级排序的无界阻塞队列。 |
DelayQueue | 一个使用优先级队列实现的无界阻塞队列。 |
SynchronousQueue | 一个不存储元素的阻塞队列。 |
LinkedTransferQueue | 一个由链表结构组成的无界阻塞队列。 |
LinkedBlockingDeque | 一个由链表结构组成的双向阻塞队列。 |
ArrayBlockingQueue
- ArrayBlockingQueue 是一个用 数组 实现的 有界 阻塞队列。
- 此队列按照先进先出(FIFO)的原则对元素进行排序。
- 在创建对象时必须指定容量大小。
- 默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。
- 在创建 ArrayBlockingQueue 时,可以设置对象的内部锁是否采用公平锁,默认采用 非公平锁。
- 公平的阻塞队列,使用了 重入锁(ReentrantLock) 实现。
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); notEmpty = lock.newCondition(); notFull = lock.newCondition(); }
- 在 ArrayBlockingQueue 内部,维护了一个 定长数组(items),以便缓存队列中的数据对象,在生产者放入数据和消费者获取数据,都是共用同一个锁对象(lock),在插入或删除元素时不会产生或销毁任何额外的对象实例。
LinkedBlockingQueue
- LinkedBlockingQueue 是一个用 链表 实现的 有界 阻塞队列。
- 此队列的默认和最大长度为 Integer.MAX_VALUE。
- 此队列按照先进先出的原则对元素进行排序。
- 创建对象时如果不指定容量大小,则使用默认大小。
public LinkedBlockingQueue(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; last = head = new Node<E>(null); }
- 同 ArrayListBlockingQueue 类似,其内部也维持着一个数据缓冲队列(该队列 由一个链表构成)
- 当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回。
- 只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue 可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。
- LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
- 值得注意的是,如果构造一个 LinkedBlockingQueue 对象,而没有指定其容量大小,LinkedBlockingQueue 会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
- 在插入或删除元素时会生成一个额外的 Node 对象。
PriorityBlockingQueue
- PriorityBlockingQueue 是基于 优先级 的阻塞队列(优先级的判断通过构造函数传入的 Compator 对象决定),需要注意的是 PriorityBlockingQueue 并 不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。
- 因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。
- 在实现 PriorityBlockingQueue 时,内部控制线程同步的锁采用的是 公平锁。
public PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1) throw new IllegalArgumentException(); this.lock = new ReentrantLock(); this.notEmpty = lock.newCondition(); this.comparator = comparator; this.queue = new Object[initialCapacity]; }
DelayQueue
- DelayQueue 是一个支持 延时获取元素 的 无界 阻塞队列。
- 队列使用 PriorityQueue 来实现。
- 队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。
- 只有在延迟期满时才能从队列中提取元素。
- DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
Deque 接口
- Deque 是一个双端队列,既支持在队头执行入队出队,也支持在队尾执行出队。
- 而 Queue 只支持在队头出队,在队尾入队。
队头入队 | 队头出队 | 队尾入队 | 队尾出队 | 队头检索 | 队尾检索 | 处理方式 |
---|---|---|---|---|---|---|
addFirst() | removeFirst() | addLast() | removeLast() | getFirst() | getLast() | 在方法执行失败时会抛出异常 |
offerFirst() | pollFirst() | offerLast() | pollLast() | peekFirst() | peekLast() | 在方法执行失败时会返回 false 或者 null。 |
- DelayQueue 可以运用在以下应用场景。
- 缓存系统的设计。
- 使用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询 DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期到了。
- 定时任务调度。
- 使用 DelayQueue 保存当天将会执行的任务和执行时间,一旦从 DelayQueue 中获取到任务就开始执行,比如 TimerQueue 就是使用 DelayQueue 实现的。
- 缓存系统的设计。
SynchronousQueue
- SynchronousQueue 是一种 无缓冲 的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么都在集市等待。
- 相对于有缓冲的 BlockingQueue 来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖)。
- 但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。
- 声明一个 SynchronousQueue 有两种不同的方式,它们之间有着不太一样的行为。
- 如果采用公平模式,SynchronousQueue 会采用公平锁,并配合一个 FIFO 队列来阻塞多余的生产者和消费者,从而体现整体的公平策略。
- 如果是非公平模式(SynchronousQueue 默认),SynchronousQueue 采用非公平锁,同时配合一个 LIFO 队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
- SynchronousQueue 非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue 的吞吐量高于 LinkedBlockingQueue 和 ArrayBlockingQueue。
LinkedTransferQueue
- LinkedTransferQueue 是一个由 链表 结构组成的 无界 阻塞 TransferQueue 队列。
- 相对于其他阻塞队列 LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。
- transfer 方法。如果当前有消费者正在等待接收元素(消费者使用
take()
方法或带时间限制的poll()
方法时),transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者。- 如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 结点,并等到该元素被消费者消费了才返回。
- tryTransfer 方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否接收,方法立即返回。而 transfer 方法是必须等到消费者消费了才返回。
- 对于带有时间限制的 tryTransfer(E e, long timeout, TimeUnit unit) 方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回 false,如果在超时时间内消费了元素,则返回 true。
- transfer 方法。如果当前有消费者正在等待接收元素(消费者使用
LinkedBlockingDeque
- LinkedBlockingDeque 是一个由 链表 结构组成的 双向 阻塞队列。
- 所谓双向队列指的可以从队列的两端插入和移出元素。
- 双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
- BlockingDeque 在 Deque 的基础上增加了阻塞的方法。
BlockingDeque 接口
阻塞队头入队 | 阻塞队头出队 | 阻塞队尾入队 | 阻塞队尾出队 | 处理方式 |
---|---|---|---|---|
putFirst(E e) | E takeFirst() | putLast(E e) | E takeLast() | 没有超时设置 |
offerFirst(E e,long timeout,TimeUnit unit) | E pollFirst(long timeout,TimeUnit unit) | offerLast(E e,long timeout,TimeUnit unit) | E pollLast(long timeout,TimeUnit unit) | 在超时之后,返回 false 或者 null。 |
- 以 First 单词结尾的方法,表示插入,获取(peek)或移除双端队列的第一个元素。
- 以 Last 单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。
- 另外插入方法 add 等同于 addLast,移除方法 remove 等效于 removeFirst。
- 在初始化 LinkedBlockingDeque 时可以初始化队列的容量,用来防止其再扩容时过渡膨胀。
3. 阻塞队列的实现原理
3.1 BlockingQueue
- BlockingQueue 中不允许有 null 元素,因此在
add()
,offer()
,put()
时如果参数是 null,会抛出空指针。null 是用来有异常情况时做返回值的。
public interface BlockingQueue<E> extends Queue<E> { //添加失败时会抛出异常 boolean add(E e); //添加失败时会返回 false 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(); //移除队列中某个元素,如果存在的话返回 true,否则返回 false boolean remove(Object o); //检查队列中是否包含某个元素,至少包含一个就返回 true public boolean contains(Object o); //将当前队列所有元素移动到给定的集合中,这个方法比反复地获取元素更高效 //返回移动的元素个数 int drainTo(Collection<? super E> c); //移动队列中至多 maxElements 个元素到指定的集合中 int drainTo(Collection<? super E> c, int maxElements); }
3.2 ArrayBlockingQueue
- ArrayBlockingQueue 使用可重入锁 ReentrantLock 实现的访问公平性,两个 Condition 保证了添加和获取元素的并发控制。
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable { //使用数组保存的元素 final Object[] items; //下一次取元素的索引 int takeIndex; //下一次添加元素的索引 int putIndex; //当前队列中元素的个数 int count; /* * Concurrency control uses the classic two-condition algorithm * found in any textbook. */ //全部操作的锁 final ReentrantLock lock; //等待获取元素的锁 private final Condition notEmpty; //等待添加元素的锁 private final Condition notFull; //... }
构造函数
//指定队列的容量,使用非公平锁 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(); } //允许使用一个 Collection 来作为队列的默认元素 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) { //遍历添加指定集合的元素 if (e == null) throw new NullPointerException(); items[i++] = e; } } catch (ArrayIndexOutOfBoundsException ex) { throw new IllegalArgumentException(); } count = i; putIndex = (i == capacity) ? 0 : i; //修改 putIndex 为 c 的容量 +1 } finally { lock.unlock(); } }
- 有三种构造函数
- 默认的构造函数只指定了队列的容量,设置为非公平的线程访问策略。
- 第二种构造函数中,使用 ReentrantLock 创建了 2 个 Condition 锁。
- 第三种构造函数可以在创建队列时,将指定的元素添加到队列中。
add 方法
add(E)
调用了父类的方法,而父类里调用的是offer(E)
,如果返回 false 就抛出异常。
public boolean add(E e) { return super.add(e); } //super.add() 的实现 public boolean add(E e) { if (offer(e)) return true; else throw new IllegalStateException("Queue full"); }
offer 方法
offer(E)
方法先拿到锁,如果当前队列中元素已满,就立即返回 false。- 如果没满就调用
enqueue(E)
入队。
- 如果没满就调用
public boolean offer(E e) { if (e == null) throw new NullPointerException(); final ReentrantLock lock = this.lock; lock.lock(); try { if (count == items.length) return false; else { enqueue(e); return true; } } finally { lock.unlock(); } }
enqueue(E)
方法会将元素添加到数组队列尾部。- 如果添加元素后队列满了,就修改 putIndex 为 0。
- 添加后调用
notEmpty.signal()
通知唤醒阻塞在获取元素的线程。
private void enqueue(E x) { final Object[] items = this.items; items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++; notEmpty.signal(); }
put 方法
put()
方法可以响应中断(lockInterruptibly
),当队列满了,就调用notFull.await()
阻塞等待,等有消费者获取元素后继续执行, 可以添加时还是调用enqueue(E)
。
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) notFull.await(); enqueue(e); } finally { lock.unlock(); } }
offer(E,long,TimeUnit) 方法
public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { if (e == null) throw new NullPointerException(); long nanos = unit.toNanos(timeout); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) { if (nanos <= 0) return false; nanos = notFull.awaitNanos(nanos); } enqueue(e); return true; } finally { lock.unlock(); } }
offer()
和put()
方法很相似,不同之处在于允许设置等待超时时间,超过这么久如果还不能有位置,就返回 false;否则调用enqueue(E)
,然后返回 true。
poll 方法
poll()
如果在队列中没有元素时会立即返回 null,如果有元素调用dequeue()
。
public E poll() { final ReentrantLock lock = this.lock; lock.lock(); try { return (count == 0) ? null : dequeue(); } finally { lock.unlock(); } }
- 默认情况下
dequeue()
方法会从队首移除元素(即 takeIndex 位置)。 - 移除后会向后移动 takeIndex,如果已经到队尾,则归零。
- 结合前面添加元素时的归零,可以看到,其实 ArrayBlockingQueue 是个环形数组。
private E dequeue() { 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; }
- 然后调用
itrs. elementDequeued()
,这个 itrs 是 ArrayBlockingQueue 的内部类 Itrs 的对象,是个迭代器,它的作用是保证循环数组迭代时的正确性。
take 方法
take()
方法可以响应中断,与poll()
不同的是,如果队列中没有数据会一直阻塞等待,直到中断或者有元素,有元素时还是调用dequeue()
方法。
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) notEmpty.await(); return dequeue(); } finally { lock.unlock(); } }
poll(long,TimeUnit) 方法
- 带参数的
poll()
方法相当于无参poll()
和take()
的中和版,允许阻塞一段时间,如果在阻塞一段时间还没有元素进来,就返回 null。
public E poll(long timeout, TimeUnit unit) throws InterruptedException { long nanos = unit.toNanos(timeout); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) { if (nanos <= 0) return null; nanos = notEmpty.awaitNanos(nanos); } return dequeue(); } finally { lock.unlock(); } }
peek 方法
peel()
直接返回数组中队尾的元素,并不删除元素。- 如果队列中没有元素返回的是 null。
public E peek() { final ReentrantLock lock = this.lock; lock.lock(); try { return itemAt(takeIndex); // null when queue is empty } finally { lock.unlock(); } } final E itemAt(int i) { return (E) items[i]; }
总结
- 一旦创建,则容量不能再改动。
- 这个类是线程安全的,并且迭代器也是线程安全的。
- 这个类的
put
和take
方法分别会在队列满了和队列空了之后被阻塞操作。 - 这个类提供了
offer
和poll
方法来插入和提取元素,而不会在队列满了或者队列为空时阻塞操作。 - 这个队列的锁默认是 不公平 策略,即唤醒线程的顺序是不确定的。
3.3 LinkedBlockingQueue
- LinkedBlockingQueue 中有 两个 ReentrantLock,一个用于添加另一个用于获取,这和 ArrayBlockingQueue 不同。
//链表结点 static class Node<E> { E item; //对当前结点的后一个结点,有三种情况: //1.真正的结点 //2.当前结点本身,说明当前结点是头结点 //3.null,说明这个结点是尾结点 Node<E> next; Node(E x) { item = x; } } //当前容量,默认是 Integer.MAX_VALUE private final int capacity; //当前队列中的元素数量 private final AtomicInteger count = new AtomicInteger(); //队列中的头结点,头结点的.item 永远为 null transient Node<E> head; //队列的尾结点,尾结点的 next 永远为 null private transient Node<E> last; //获取元素的锁 private final ReentrantLock takeLock = new ReentrantLock(); //等待取元素的等待队列 private final Condition notEmpty = takeLock.newCondition(); //添加元素的锁 private final ReentrantLock putLock = new ReentrantLock(); //等待添加元素的等待队列 private final Condition notFull = putLock.newCondition();
构造函数
//使用 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 作为容量,同时将指定集合添加到队列中 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(); } }
put 方法
- LinkedBlockingQueue 使用了 AtomicInteger 类型的 count 保存队列元素个数,在添加时,如果队列满了就阻塞等待。
- 有两种继续执行的情况。
- 有消费者取元素,count 会减少,小于队列容量。
- 或者调用了 notFull.signal() 。
- 有两种继续执行的情况。
- ArrayBlockingQueue 中放入数据阻塞的时候,需要消费数据才能唤醒。
- 而 LinkedBlockingQueue 中放入数据阻塞的时候,因为内部有 2 个锁,可以并行执行放入数据和消费数据,不仅在消费数据的时候进行唤醒插入阻塞的线程,同时在插入的时候如果容量还没满,也会唤醒插入阻塞的线程。
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // 不允许空元素 int c = -1; Node<E> node = new Node(e); // 以新元素构造结点 final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); // 放锁加锁,保证调用put方法的时候只有1个线程 try { while (count.get() == capacity) { // 如果容量满了 notFull.await(); // 阻塞并挂起当前线程 } enqueue(node); // 结点添加到链表尾部 c = count.getAndIncrement(); // 元素个数+1 if (c + 1 < capacity) // 如果容量还没满 notFull.signal(); // 在放锁的条件对象notFull上唤醒正在等待的线程,表示可以再次往队列里面加数据了,队列还没满 } finally { putLock.unlock(); // 释放放锁,让其他线程可以调用put方法 } if (c == 0) // 由于存在放锁和拿锁,这里可能拿锁一直在消费数据,count会变化。这里的if条件表示如果队列中还有1条数据 signalNotEmpty(); // 在拿锁的条件对象notEmpty上唤醒正在等待的1个线程,表示队列里还有1条数据,可以进行消费 }
- 入队调用的
enqueue()
链表尾部添加结点。
private void enqueue(Node<E> node) { last = last.next = node; }
- 在入队后,还会判断一次队列中的元素个数,如果此时小于队列容量,唤醒其他阻塞的添加线程。
- 最后还会判断容量,如果这时队列中有 1 个元素的情况,就通知 notEmpty 上阻塞的线程。
private void signalNotEmpty() { final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { notEmpty.signal(); } finally { takeLock.unlock(); } }
offer 方法
- offer 直接返回结果
public boolean offer(E e) { if (e == null) throw new NullPointerException(); // 不允许空元素 final AtomicInteger count = this.count; if (count.get() == capacity) // 如果容量满了,返回false return false; int c = -1; Node<E> node = new Node(e); // 容量没满,以新元素构造结点 final ReentrantLock putLock = this.putLock; putLock.lock(); // 放锁加锁,保证调用offer方法的时候只有1个线程 try { if (count.get() < capacity) { // 再次判断容量是否已满,因为可能拿锁在进行消费数据,没满的话继续执行 enqueue(node); // 结点添加到链表尾部 c = count.getAndIncrement(); // 元素个数+1,并返回旧值 if (c + 1 < capacity) // 如果容量还没满 notFull.signal(); // 在放锁的条件对象notFull上唤醒正在等待的线程,表示可以再次往队列里面加数据了,队列还没满 } } finally { putLock.unlock(); // 释放放锁,让其他线程可以调用offer方法 } if (c == 0) // 如果队列中还有1条数据 signalNotEmpty(); // 在拿锁的条件对象notEmpty上唤醒正在等待的1个线程,表示队列里还有1条数据,可以进行消费 return c >= 0; // 添加成功返回true,否则返回false }
offer(E,long,TimeUnit) 方法
- 和 ArrayBlockingQueue 一样,带阻塞时间参数的
offer()
方法会阻塞一段时间,然后没结果就返回。
take 方法
- 这里和前面一样,都使用的是
AtomicInteger.getAndDecrement()
方法,这个方法先返回当前值,然后加 1 ,所以后面判断是判断之前的情况。
public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); // 拿锁加锁,保证调用take方法的时候只有1个线程 try { while (count.get() == 0) { // 如果队列里已经没有元素了 notEmpty.await(); // 阻塞并挂起当前线程 } x = dequeue(); // 删除头结点 c = count.getAndDecrement(); // 元素个数-1 if (c > 1) // 如果队列里还有元素 notEmpty.signal(); // 在拿锁的条件对象notEmpty上唤醒正在等待的线程,表示队列里还有数据,可以再次消费 } finally { takeLock.unlock(); // 释放拿锁,让其他线程可以调用take方法 } if (c == capacity) // 如果队列中还可以再插入数据 signalNotFull(); // 在放锁的条件对象notFull上唤醒正在等待的1个线程,表示队列里还能再次添加数据 return x; }
- LinkedBlockingQueue 的 take 方法对于没数据的情况下会阻塞,poll 方法删除链表头结点,remove 方法删除指定的对象。
- 需要注意的是 remove 方法由于要删除的数据的位置不确定,需要 2 个锁同时加锁。
public boolean remove(Object o) { if (o == null) return false; fullyLock(); // remove操作要移动的位置不固定,2个锁都需要加锁 try { for (Node<E> trail = head, p = trail.next; // 从链表头结点开始遍历 p != null; trail = p, p = p.next) { if (o.equals(p.item)) { // 判断是否找到对象 unlink(p, trail); // 修改结点的链接信息,同时调用notFull的signal方法 return true; } } return false; } finally { fullyUnlock(); // 2个锁解锁 } }
- 队首元素出队
private E dequeue() { Node<E> h = head; Node<E> first = h.next; h.next = h; // help GC head = first; //指向队首的结点后移 E x = first.item; first.item = null; return x; }
- LinkedBlockingQueue 比 ArrayBlockingQueue 的优势就是添加和获取是两个不同的锁,所以并发添加/获取效率更高些。
- 数组元素个数用的是 AtomicInteger 类型的,这样在添加、获取时通过判断数组元素个数可以感知到并发的获取/添加操作。
- 此外链表比数组的优势。
3.4 PriorityBlockingQueue
- PriorityBlockingQueue 是基于 数组 的、支持 优先级 的、无界 阻塞队列。
- 通过使用堆这种数据结构实现将队列中的元素按照某种排序规则进行排序,从而改变先进先出的队列顺序,提供开发者改变队列中元素的顺序的能力。
- 堆是一种二叉树结构,堆的根元素是整个树的最大值或者最小值(称为大顶堆或者小顶堆),同时堆的每个子树都是满足堆的树结构。
- 由于堆的顶部是最大值或者最小值,所以每次从堆获取数据都是直接获取堆顶元素,然后再将堆调整成堆结构。
- 默认情况队列中的元素按自然排序升序排列,也可以实现元素的
compareTo()
指定元素的排序规则,或者在初始化它时在构造函数中传递 Comparator 排序规则。 - 不能保证同一优先级元素的顺序。
- 通过使用堆这种数据结构实现将队列中的元素按照某种排序规则进行排序,从而改变先进先出的队列顺序,提供开发者改变队列中元素的顺序的能力。
构造函数
- PriorityBlockingQueue 在初始化时创建指定容量的数组,默认是 11 。
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; public PriorityBlockingQueue() { this(DEFAULT_INITIAL_CAPACITY, null); } public PriorityBlockingQueue(int initialCapacity) { this(initialCapacity, null); } public PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1) throw new IllegalArgumentException(); this.lock = new ReentrantLock(); this.notEmpty = lock.newCondition(); this.comparator = comparator; this.queue = new Object[initialCapacity]; }
offer 方法
- 是 无界 阻塞队列。
- 添加元素时,当数组中元素大于等于容量时,会调用
tryGrow()
扩容。
- 添加元素时,当数组中元素大于等于容量时,会调用
- 在扩容时,如果当前队列中元素个数小于 64 个,数组容量就乘 2 加 2。否则变成原来的 1.5 倍(容量越大,扩容成本越高,所以容量设置的小一些)。
private void tryGrow(Object[] array, int oldCap) { lock.unlock(); // must release and then re-acquire main lock Object[] newArray = null; if (allocationSpinLock == 0 && UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, 0, 1)) { try { int newCap = oldCap + ((oldCap < 64) ? (oldCap + 2) : // grow faster if small (oldCap >> 1)); if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow int minCap = oldCap + 1; if (minCap < 0 || minCap > MAX_ARRAY_SIZE) throw new OutOfMemoryError(); newCap = MAX_ARRAY_SIZE; } if (newCap > oldCap && queue == array) newArray = new Object[newCap]; //扩容数组 } finally { allocationSpinLock = 0; } } if (newArray == null) // back off if another thread is allocating Thread.yield(); lock.lock(); if (newArray != null && queue == array) { queue = newArray; System.arraycopy(array, 0, newArray, 0, oldCap); //拷贝原有数据 } }
保证优先级
- 没有设置 Comparator 默认调用的是
siftUpComparable()
。- 每从队尾添加一个元素都会从下往上挨个比较自己和 " 父结点 " 的大小,如果小就交换,否则就停止。
private static <T> void siftUpComparable(int k, T x, Object[] array) { Comparable<? super T> key = (Comparable<? super T>) x; while (k > 0) { // 循环比较 // 寻找k的父元素下标,固定规则 int parent = (k - 1) >>> 1; Object e = array[parent]; // 自下而上一般出现在插入元素时调用,插入元素是插入到队列的最后,则需要将该元素调整到合适的位置 // 即从队列的最后往上调整堆,直到不小于其父结点为止,相当于冒泡 if (key.compareTo((T) e) >= 0) //比较 break; // 如果当前结点<其父结点,则将其与父结点进行交换,并继续往上访问父结点 array[k] = e; k = parent; } array[k] = key; }
poll 方法
- 出队时同样进行了两种不同的比较。
public E poll() { // size==0队列为0,直接返回null if (size == 0) return null; int s = --size; modCount++; // 出队总是将数组的第一个元素进行出队, E result = (E) queue[0]; E x = (E) queue[s]; queue[s] = null; if (s != 0) // 同时将队列的最后一个元素放到第一个位置,然后自上而下调整堆 siftDown(0, x); return result; }
- 出队时同样进行了两种不同的比较。
- 取元素时,在移除第一个元素后,会用堆排序将当前堆再排一次序。
private void siftDownUsingComparator(int k, E x) { // 由于堆是一个二叉树,所以size/2是树中的最后一个非叶子结点 // 如果k是叶子结点,那么其无子结点,则不需要再往下调整堆 int half = size >>> 1; while (k < half) { int child = (k << 1) + 1; Object c = queue[child]; // 右结点 int right = child + 1; // 找出两个子结点以及父结点中较小的一个 if (right < size && comparator.compare((E) c, (E) queue[right]) > 0) c = queue[child = right]; // 如果父结点最小,则无需继续往下调整堆 if (comparator.compare(x, (E) c) <= 0) break; // 否则将父结点与两个子结点中较小的一个交换,然后往下继续调整 queue[k] = c; k = child; } queue[k] = x; }
- PriorityBlockingQueue 是无界、有优先级的队列。
- 可以扩容,在添加、删除元素后都会进行排序。
3.5 DelayQueue
- DelayQueue 是一个支持 延时 获取元素的、无界 阻塞队列。
- 队列使用 PriorityQueue 实现,队列中的元素必须实现 Delayed 接口。
- 实现 Delayed 的类也需要实现 Comparable 接口,即实现
compareTo()
方法,保证集合中元素的顺序和getDelay()
一致。
- 实现 Delayed 的类也需要实现 Comparable 接口,即实现
public interface Delayed extends Comparable<Delayed> { //返回当前对象的剩余执行时间 long getDelay(TimeUnit unit); }
- 创建元素时可以指定多久才能从队列中获取当前元素。
3.5.1 DelayQueue 的关键属性
private final transient ReentrantLock lock = new ReentrantLock(); private final PriorityQueue<E> q = new PriorityQueue<E>(); private Thread leader; /** * Condition signalled when a newer element becomes available * at the head of the queue or a new thread may need to * become leader. */ private final Condition available = lock.newCondition();
属性 | 说明 |
---|---|
ReentrantLock lock | 重入锁。 |
PriorityQueue q | 无界的、优先级队列。 |
Thread leader | Leader-Follower 模型中的 leader |
Condition available | 队首有新元素可用或者有新线程成为 leader 时触发的 condition。 |
PriorityQueue
- PriorityQueue 是一个用 数组 实现的,基于二叉堆(元素[n] 的子孩子是 元素[2n+1] 和元素[2(n+1)] )数据结构的集合。
- 在添加元素时如果超出限制也会扩容,是 无界 的。
public boolean offer(E e) { if (e == null) throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) grow(i + 1); size = i + 1; if (i == 0) queue[0] = e; else siftUp(i, e); return true; }
Leader-Follower 模型
- 这种模型中所有线程分为三种身份 Leader、Follower、Proccesser。
- 基本原则是最多只有一个 Leader。所有 Follower 都在等待成为 Leader。
- 队列中的 leader 是一个等待获取队列头部元素的线程。
- 如果 leader 不等于空,表示已经有线程在等待获取队列的头元素,然后使用
await()
方法让当前线程等待信号。 - 如果 leader 为空,则把当前线程设置成 leader,并使用
awaitNanos()
方法让当前线程等待接收信号或等待 delay 时间。
- 如果 leader 不等于空,表示已经有线程在等待获取队列的头元素,然后使用
- 这种方法可以增强 CPU 高速缓存相似性,及消除动态内存分配和线程间的数据交换。这种模式是为了最小化任务等待时间,当一个线程成为 leader 后,它只需要等待下一个可执行任务的出现,而其他线程要无限制地等待。
实现 Delayed 接口
- DelayQueue 的元素必须实现 Delayed 接口,实现 Delayed 接口大概有三步。
- 构造函数中初始化基本数据,比如执行时间等数据。
- 实现
getDelay()
方法,返回当前元素还需要延时多久执行。 - 实现
compareTo()
方法,指定不同元素如何比较谁先执行。
延时阻塞队列的实现
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { for (;;) { E first = q.peek(); //先获取队首元素,不删除 if (first == null) //如果为空就阻塞等待 available.await(); else { long delay = first.getDelay(NANOSECONDS); if (delay <= 0L) //比较元素延时时间是否到达 return q.poll(); //如果是就移除并返回 first = null; // don't retain ref while waiting if (leader != null) //如果有 leader 线程,依然阻塞等待 available.await(); else { //如果没有 leader 线程,指定当前线程,然后等待任务的待执行时间 Thread thisThread = Thread.currentThread(); leader = thisThread; try { available.awaitNanos(delay); } finally { if (leader == thisThread) leader = null; } } } } } finally { //最后等待时间到了后,就通知阻塞的线程 if (leader == null && q.peek() != null) available.signal(); lock.unlock(); } } //PriorityQueue.peek() public E peek() { return (size == 0) ? null : (E) queue[0]; }
- 取元素时,会根据元素的延时执行时间是否为 0 进行判断,如果延时执行时间已经没有了,就直接返回,否则需要等待执行时间到达后再返回。
3.6 SynchronousQueue
- SynchronousQueue 支持公平访问队列,根据构造函数的参数不同,有两种实现方式:TransferQueue 和 TransferStack,默认情况下是 false。
private transient volatile Transferer<E> transferer; public SynchronousQueue() { this(false); } public SynchronousQueue(boolean fair) { transferer = fair ? new TransferQueue<E>() : new TransferStack<E>(); }
- SynchronousQueue 是一个 不存储元素 的阻塞队列。
- 这里的 " 不存储元素 " 指的是,SynchronousQueue 容量为 0,每添加一个元素必须等待被取走后才能继续添加元素。
put 方法
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); if (transferer.transfer(e, false, 0) == null) { Thread.interrupted(); throw new InterruptedException(); } }
- 添加是调用的
transferer.transfer()
,如果返回 null 就调用Thread.interrupted()
将中断标志位复位(设为 false),然后抛出异常。
/** * Puts or takes an item. */ @SuppressWarnings("unchecked") E transfer(E e, boolean timed, long nanos) { SNode s = null; int mode = (e == null) ? REQUEST : DATA; //判断是添加还是获取 for (;;) { SNode h = head; //获取栈顶结点 if (h == null || h.mode == mode) { // empty or same-mode if (timed && nanos <= 0) { // can't wait if (h != null && h.isCancelled()) //如果头结点无法获取,就去获取下一个 casHead(h, h.next); // pop cancelled node else return null; } else if (casHead(h, s = snode(s, e, h, mode))) { //设置头结点 SNode m = awaitFulfill(s, timed, nanos); if (m == s) { // wait was cancelled clean(s); return null; } if ((h = head) != null && h.next == s) casHead(h, s.next); // help s's fulfiller return (E) ((mode == REQUEST) ? m.item : s.item); } } else if (!isFulfilling(h.mode)) { // try to fulfill if (h.isCancelled()) // already cancelled casHead(h, h.next); // pop and retry else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) { for (;;) { // loop until matched or waiters disappear SNode m = s.next; // m is s's match if (m == null) { // all waiters are gone casHead(s, null); // pop fulfill node s = null; // use new node next time break; // restart main loop } SNode mn = m.next; if (m.tryMatch(s)) { casHead(s, mn); // pop both s and m return (E) ((mode == REQUEST) ? m.item : s.item); } else // lost match s.casNext(m, mn); // help unlink } } } else { // help a fulfiller SNode m = h.next; // m is h's match if (m == null) // waiter is gone casHead(h, null); // pop fulfilling node else { SNode mn = m.next; if (m.tryMatch(h)) // help match casHead(h, mn); // pop both h and m else // lost match h.casNext(m, mn); // help unlink } } } }
- 主要步骤。
- 栈是空的或者栈顶元素的模式和当前要进行的操作一致。
- 将结点推到堆栈上并等待匹配。
- 等待参数中的时间后返回。
- 如果取消就返回 null。
- 如果栈不为空且栈顶元素模式与当前要进行的操作不一致,如果这个元素的模式是相反的模式。
- 尝试将栈中一个模式匹配要求的结点推到堆栈上,与相应的等待结点匹配并返回。
- 如果栈顶已经拥有另一个模式匹配的结点。
- 通过执行 POP 操作来找到匹配的元素,然后继续。
- 栈是空的或者栈顶元素的模式和当前要进行的操作一致。
- 简单概括就是一个添加操作后必须等待一个获取操作才可以继续添加。
结论
- LinkedBlockingQueue 性能表现远超 ArrayBlcokingQueue,不管线程多少,不管 Queue 长短,LinkedBlockingQueue 都胜过 ArrayBlockingQueue。
- SynchronousQueue 表现很稳定,而且在 20 个线程之内不管 Queue 长短,SynchronousQueue 性能表现是最好的,(其实 SynchronousQueue 跟 Queue 长短没有关系),如果 Queue 的 capability 只能是 1,那么毫无疑问选择 SynchronousQueue,这也是设计 SynchronousQueue 的目的。
- 当超过 1000 个线程时,SynchronousQueue 性能就直线下降,只有最高峰的一半左右,而且当 Queue 大于 30 时,LinkedBlockingQueue 性能就超过 SynchronousQueue。
- 相较于其他队列有缓存的作用,SynchronousQueue 适用于单线程同步传递性场景,比如:消费者没拿走当前的产品,生产者是不能再给产品,这样可以控制生产者生产的速率和消费者一致。
3.7 LinkedTransferQueue
- LinkedTransferQueue 实现了 TransferQueue 接口, 是一个由 链表 组成的、无界 阻塞队列。
public class LinkedTransferQueue<E> extends AbstractQueue<E> implements TransferQueue<E>, java.io.Serializable {...}
TransferQueue
- TransferQueue 也是一种阻塞队列,用于生产者需要等待消费者消费事件的场景,与 SynchronousQueue 有相似之处。
public interface TransferQueue<E> extends BlockingQueue<E> { //尽可能快地转移元素给一个等待的消费者 //如果在这之前有其他线程调用了 taked() 或者 poll(long,TimeUnit) 方法,就返回 true //否则返回 false boolean tryTransfer(E e); //转移元素给一个消费者,在有的情况下会等待直到被取走 void transfer(E e) throws InterruptedException; //在 timeout 时间内将元素转移给一个消费者,如果这段时间内传递出去了就返回 true //否则返回 false boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException; //如果至少有一个等待的消费者,就返回 true boolean hasWaitingConsumer(); //返回等待获取元素的消费者个数 //这个值用于监控 int getWaitingConsumerCount(); }
- 相对于其他阻塞队列,LinkedTransferQueue 多了两个关键地方法
tryTransfer()
和transfer()
。
transfer 方法
transfer()
方法的作用是如果有等待接收元素的消费者线程,直接把生产者传入的元素 transfer 给消费者。- 如果没有消费者线程,
transfer()
会将元素存放到队列尾部,并等待元素被消费者取走才返回。
Node pred = tryAppend(s, haveData); return awaitMatch(s, pred, e, (how == TIMED), nanos);
awaitMatch()
方法的作用是,CPU 自旋等待消费者取走元素,为了避免长时间消耗 CPU,在自旋一定次数后会调用 Thread.yield() 暂停当前正在执行的线程,改为执行其他线程。
tryTransfer 方法
tryTransfer()
的作用是试探生产者传入的元素是否能直接传递给消费者。- 如果有等待接收的消费者,返回 true。
- 没有则返回 false。
public boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException { if (xfer(e, true, TIMED, unit.toNanos(timeout)) == null) return true; if (!Thread.interrupted()) return false; throw new InterruptedException(); }
- 和
transfer()
必须等到消费者取出元素才返回不同的是,tryTransfer()
无论是否有消费者接收都会立即返回。 - 对于带有时间限制的
tryTransfer(E,long,TimeUnit)
方法,试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回 false,如果在超时时间内消费了元素,则返回 true。
3.8 LinkedBlockingDeque
- LinkedBlockingDeque 是一个由 链表 组成的、双向 阻塞队列。
关键属性
static final class Node<E> { E item; Node<E> prev; Node<E> next; Node(E x) { item = x; } } transient Node<E> first; transient Node<E> last; private transient int count; private final int capacity; final ReentrantLock lock = new ReentrantLock(); private final Condition notEmpty = lock.newCondition(); private final Condition notFull = lock.newCondition();
- LinkedBlockingDeque 中持有队列首部和尾部结点,每个结点也是双向的。
- 双向的作用是可以从队列两端插入和移除元素。多了一个操作队列的方向,在多线程同时入队时,可以减少一半的竞争。
- 除了
remove(Object)
等移除操作,LinkedBlockingDeque 的大多数操作的时间复杂度都是 O(n)。
4. 阻塞队列的特点
- 阻塞队列使用最经典的场景就是 socket 客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。
- 还有其他类似的场景,只要符合 生产者-消费者模型 的都可以使用阻塞队列。
ArrayBlockingQueue
- 环形数组 实现的、有界 的队列,一旦创建后,容量不可变。
- 基于数组,在添加删除上 性能不如链表。
LinkedBlockingQueue
- 基于 链表、有界 阻塞队列。
- 添加和获取是 两个不同的锁,所以 并发添加/获取效率更高些。
Executors.newFixedThreadPool()
使用了这个队列。
PriorityBlockingQueue
- 基于 数组 的、支持 优先级 的、无界 阻塞队列。
- 使用 自然排序或者定制排序 指定排序规则。
- 添加元素时,当数组中元素 大于等于容量 时,会 扩容(当前队列中元素个数小于 64 个,数组容量 乘 2 加 2,否则变成原来的 1.5 倍),拷贝数组。
DelayQueue
- 支持 延时获取元素 的、无界 阻塞队列。
- 添加元素时如果超出限制也会 扩容。
- 采用 Leader-Follower 模型。
SynchronousQueue
- 容量为 0。
- 一个添加操作后 必须等待 一个获取操作才可以继续添加。
- 吞吐量高于 LinkedBlockingQueue 和 ArrayBlockingQueue。
LinkedTransferQueue
- 由 链表 组成的、无界 阻塞队列。
- 实现了 TransferQueue 接口。
- CPU 自旋 等待消费者取走元素,自旋一定次数后结束。
LinkedBlockingDeque
- 由 双向链表 组成的、双向 阻塞队列。
- 可以从队列 两端 插入和移除元素。
- 多了一个操作队列的方向,在多线程同时入队时,可以 减少一半 的竞争。
参考资料
https://blog.csdn.net/u011240877/article/details/73612930#1arrayblockingqueue
https://blog.csdn.net/fuyuwei2015/article/details/72716753
https://blog.csdn.net/tonywu1992/article/details/83419448