并发编程从入门到放弃系列开始和结束(八)
阻塞队列
并发编程中,队列是其中不可缺少的一环,其实前面在说到线程池的时候,就已经提及到了阻塞队列了,这里我们要一起看看 JUC 包下提供的这些队列。
阻塞队列
阻塞队列中的阻塞包含两层意思:
- 插入的时候,如果阻塞队列满,插入元素阻塞
- 删除/查询的时候,如果阻塞队列空,删除/查询元素阻塞
下面列出队列的一些插入和删除元素的方法,一个个来说:
add:向队列尾部插入元素,插入成功返回 true,队列满则抛出IllegalStateException("Queue full")异常
offer:向队列尾部插入元素,队列满返回 false,否则返回 true,带超时的则是会阻塞,达到超时时间后返回
put:向队列尾部插入元素,队列满会一直阻塞
remove:删除队列头部元素,删除成功返回 true,队列空则抛出NoSuchElementException异常
poll:删除队列头部元素,删除成功返回队列头部元素,队列空返回null,带超时的则是会阻塞,达到超时时间后返回
take:删除队列头部元素,队列空会一直阻塞
element:查询队列头部元素,并且返回,队列空则抛出NoSuchElementException异常
peek:查询队列头部元素,并且返回
ArrayBlockingQueue
ArrayBlockingQueue 从名字就知道,基于数组实现的有界阻塞队列,基于AQS支持公平和非公平策略。
还是看构造函数吧,可以传入初始数组大小,一旦设置之后大小就不能改变了,传参可以支持公平和非公平,最后一个构造函数可以支持传入集合进行初始化,但是长度不能超过 capacity,否则抛出ArrayIndexOutOfBoundsException异常。
public ArrayBlockingQueue(int capacity);
public ArrayBlockingQueue(int capacity, boolean fair);
public ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c);
这个其实在上面介绍 Condition 的时候我们就已经实现过他了,这里就不再说了,可以参考上面 Condition 的部分。
LinkedBlockingQueue
LinkedBlockingQueue 基于链表实现的有界阻塞队列。
使用无参构造函数则链表长度为 Integer.MAX_VALUE,另外两个构造函数和 ArrayBlockingQueue 差不多。
public LinkedBlockingQueue();
public LinkedBlockingQueue(int capacity);
public LinkedBlockingQueue(Collection<? extends E> c);
我们可以看看 put 和 take 的源码。
- 首先加锁中断
- 然后判断如果达到了队列的最大长度,那么就阻塞等待,否则就把元素插入到队列的尾部
- 注意这里和 ArrayBlockingQueue 有个区别,这里再次做了一次判断,如果队列没满,唤醒因为 put 阻塞的线程,为什么要做判断,因为他们不是一把锁
- 最后的逻辑是一样的,notEmpty 唤醒
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); int c = -1; Node<E> node = new Node<E>(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(); } private void enqueue(Node<E> node) { // assert putLock.isHeldByCurrentThread(); // assert last.next == null; last = last.next = node; } private void signalNotEmpty() { final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { notEmpty.signal(); } finally { takeLock.unlock(); } }
take的逻辑也是非常类似啊。
- 加锁中断
- 判断队列是不是空了,空了的话就阻塞等待,否则就从队列移除一个元素
- 然后再次做一次判断,队列要是不空,就唤醒阻塞的线程
- 最后唤醒 notFull 的线程
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; } private E dequeue() { // assert takeLock.isHeldByCurrentThread(); // assert head.item == null; 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; } private void signalNotFull() { final ReentrantLock putLock = this.putLock; putLock.lock(); try { notFull.signal(); } finally { putLock.unlock(); } }
PriorityBlockingQueue
PriorityBlockingQueue 是支持优先级的无界阻塞队列,默认排序按照自然排序升序排列。
几个构造函数,无参构造函数初始容量为11,可以自定义,也可以在创建的时候传入 comparator 自定义排序规则。
public PriorityBlockingQueue();
public PriorityBlockingQueue(int initialCapacity);
public PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator);
public PriorityBlockingQueue(Collection<? extends E> c);
直接看 put 和 take 方法吧,后面都这样,其他的就忽略好了,找到 put 之后,发现直接就是调用的 offer,那我们就直接看 offer 的实现。
- 首先还是加锁,然后看当前元素个数是否达到了数组的上限,到了就调用 tryGrow 去扩容。
- 看是否实现了 Comparator 接口,是的话就用 Comparator 去排序,否则就用 Comparable 去比较,如果两个都没有,会报错
- notEmpty 唤醒,最后解锁
文章转自公众号:艾小仙