参考及引用声明:
Java多线程进阶(三一)—— J.U.C之collections框架:BlockingQueue接口
不怕难之BlockingQueue及其实现
ReentrantLock(重入锁)功能详解和应用演示
Java之BlockingQueue
BlockingQueue深入解析-BlockingQueue看这一篇就够了
ThreadPoolExecutor线程池解析与BlockingQueue的三种实现
BlockingQueue即阻塞队列,一个指定长度的队列(“先进先出”)**,如果队列满了,添加新元素的操作会被阻塞等待,直到有空位为止**。同样,当队列为空时候,请求队列元素的操作同样会阻塞等待,直到有可用元素为止。
通过加“锁”实现线程安全的一个“容器”。
场景类比:还是上厕所的例子。某单位的厕所有3个厕位,一开始,都在“等待”被上,先后进来5个人,先进来的3个人抢占到了位置,并且关门上“锁”,其他两个人就要排队“等待” 。这个时候,厕所就相当于一个长度为3的阻塞队列。
常用于实现生产者与消费者模式,“生产者”和“消费者”是相互独立的,两者之间的通信需要依靠一个队列。这个队列,其实就是所谓的“阻塞队列”。“阻塞队列”的最大好处就是解耦,使“生产者”和“消费者”之间解耦,互不影响的。
扩展:生产者-消费者模式 Producer-Consumer Pattern
生产者和消费者在为不同的处理线程,生产者必须将数据安全地交给消费者,消费者进行消费时,如果生产者还没有建立数据,则消费者需要等待。
类比的例子里,高铁站可以看成是生产者,一直源源不断“产生”要坐的士的乘客。而的士,就可以看成消费者,一直在“消费”那些乘客。
Channel从Producer参与者处接受Data参与者,并保管起来,并应Consumer参与者的要求,将Data参与者传送出去。为确保安全性,Producer参与者与Consumer参与者要对访问共享互斥。
出处:https://segmentfault.com/a/1190000015558655
BlockingQueue它是基于ReentrantLock。我们需要先了解ReentrantLock(可重入锁)。
ReentrantLock锁在同一个时间点只能被一个线程锁持有;而可重入的意思是,ReentrantLock锁,可以被单个线程多次获取。
jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。
ReentrantLock(灵活)和synchronized(隐式,自动)都是独占锁,只允许线程互斥的访问临界区
ReentrantLock和(重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样)synchronized(可以放在被递归执行,不用担心释放问题)都是可重入的
也可通过传参new ReentrantLock(true)实现公平。
(类包括ReentrantLock,LinkedList,用于唤醒和等待的Condition对象),参见ReentrantLock(重入锁)功能详解和应用演示
可以响应中断的获取锁的方法lockInterruptibly():
可以用来解决死锁问题。
BlockingQueue继承了Queue接口,可以看到,对于每种基本方法,“抛出异常”和“返回特殊值”的方法定义和Queue是完全一样的。BlockingQueue只是增加了两类和阻塞相关的方法:put(e)、take();offer(e, time, unit)、poll(time, unit)。
同时,BlockingQueue队列中不能包含null元素。
BlockingQueue的核心方法:
public interface BlockingQueue extends Queue {
//将给定元素设置到队列中,如果设置成功返回true, 否则返回false。如果是往限定了长度的队列中设置值,推荐使用offer()方法。
boolean add(E e);
//将给定的元素设置到队列中,如果设置成功返回true, 否则返回false. e的值不能为空,否则抛出空指针异常。
boolean offer(E e);
//将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。
void put(E e) throws InterruptedException;
//将给定元素在给定的时间内设置到队列中,如果设置成功返回true, 否则返回false.
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
//从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。
E take() throws InterruptedException;
//在给定的时间里,从队列中获取值,时间到了直接调用普通的poll方法,为null则直接返回null。
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
//获取队列中剩余的空间。
int remainingCapacity();
//从队列中移除指定的值。
boolean remove(Object o);
//判断队列中是否拥有该值。
public boolean contains(Object o);
//将队列中值,全部移除,并发设置到给定的集合中。
int drainTo(Collection super E> c);
//指定最多数量限制将队列中值,全部移除,并发设置到给定的集合中。
int drainTo(Collection super E> c, int maxElements);
}
BlockingQueue接口的实现类都必须是线程安全的,实现类一般通过“锁”保证线程安全;
参考:Java多线程进阶(三二)—— J.U.C之collections框架:ArrayBlockingQueue
扩展:公平锁 与 非公平锁
公平锁:加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程,先来先得。
非公平锁:线程加锁时直接尝试获取锁,获取不到就自动到队尾等待。
更多的是直接使用非公平锁:非公平锁比公平锁性能高5-10倍,因为公平锁需要在多核情况下维护一个队列,如果当前线程不是队列的第一个无法获取锁,增加了线程切换次数。
public class ArrayBlockingQueue extends AbstractQueue
implements BlockingQueue, java.io.Serializable {
/**
* 内部数组
*/
final Object[] items;
/**
* 下一个待删除位置的索引: take, poll, peek, remove方法使用
*/
int takeIndex;
/**
* 下一个待插入位置的索引: put, offer, add方法使用
*/
int putIndex;
/**
* 队列中的元素个数
*/
int count;
/**
* 全局锁
*/
final ReentrantLock lock;
/**
* 非空条件队列:当队列空时,线程在该队列等待获取
*/
private final Condition notEmpty;
/**
* 非满条件队列:当队列满时,线程在该队列等待插入
*/
private final Condition notFull;
/**
* 指定队列初始容量和公平/非公平策略的构造器.
*/
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();
}
// put方法是一个阻塞的方法,如果队列元素已满,那么当前线程将会被notFull条件对象挂起加到等待队列中,
//直到队列有空档才会唤醒执行添加操作。但如果队列没有满,
//那么就直接调用enqueue(e)方法将元素加入到数组队列中
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//当队列元素个数与数组长度相等时,无法添加元素,之所以这样做,是防止线程被意外唤醒,不经再次判断就直接调用enqueue方法。
while (count == items.length)
//将当前调用线程挂起,添加到notFull条件队列中等待唤醒
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
//入队操作
//add方法和offer方法最终调用的是enqueue(E x)方法,其方法内部通过putIndex索引直接将元素添加到数组items中,
//这里可能会疑惑的是当putIndex索引大小等于数组长度时,需要将putIndex重新设置为0
private void enqueue(E x) {
final Object[] items = this.items;
//通过putIndex索引对数组进行赋值
items[putIndex] = x;
//索引自增,如果已是最后一个位置,重新设置 putIndex = 0;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//判断队列是否为null,不为null执行dequeue()方法,否则返回null
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
//删除队列头元素并返回
private E dequeue() {
//拿到当前数组的数据
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//获取要删除的对象
E x = (E) items[takeIndex];
//将数组中takeIndex索引位置设置为null
items[takeIndex] = null;
//takeIndex索引加1并判断是否与数组长度相等,
//如果相等说明已到尽头,恢复为0
if (++takeIndex == items.length)
takeIndex = 0;
count--;//队列个数减1
if (itrs != null)
itrs.elementDequeued();//同时更新迭代器中的元素数据
//删除了元素说明队列有空位,唤醒notFull条件对象添加线程,执行添加操作
notFull.signal();
return x;
}
public boolean remove(Object o) {
if (o == null) return false;
//获取数组数据
final Object[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lock();//加锁
try {
//如果此时队列不为null,这里是为了防止并发情况
if (count > 0) {
//获取下一个要添加元素时的索引
final int putIndex = this.putIndex;
//获取当前要被删除元素的索引
int i = takeIndex;
//执行循环查找要删除的元素
do {
//找到要删除的元素
if (o.equals(items[i])) {
removeAt(i);//执行删除
return true;//删除成功返回true
}
//当前删除索引执行加1后判断是否与数组长度相等
//若为true,说明索引已到数组尽头,将i设置为0
if (++i == items.length)
i = 0;
} while (i != putIndex);//继承查找
}
return false;
} finally {
lock.unlock();
}
}
//根据索引删除元素,实际上是把删除索引之后的元素往前移动一个位置
void removeAt(final int removeIndex) {
final Object[] items = this.items;
//先判断要删除的元素是否为当前队列头元素
if (removeIndex == takeIndex) {
//如果是直接删除
items[takeIndex] = null;
//当前队列头元素加1并判断是否与数组长度相等,若为true设置为0
if (++takeIndex == items.length)
takeIndex = 0;
count--;//队列元素减1
if (itrs != null)
itrs.elementDequeued();//更新迭代器中的数据
} else {
//如果要删除的元素不在队列头部,
//那么只需循环迭代把删除元素后面的所有元素往前移动一个位置
//获取下一个要被添加的元素的索引,作为循环判断结束条件
final int putIndex = this.putIndex;
//执行循环
for (int i = removeIndex;;) {
//获取要删除节点索引的下一个索引
int next = i + 1;
//判断是否已为数组长度,如果是从数组头部(索引为0)开始找
if (next == items.length)
next = 0;
//如果查找的索引不等于要添加元素的索引,说明元素可以再移动
if (next != putIndex) {
items[i] = items[next];//把后一个元素前移覆盖要删除的元
i = next;
} else {
//在removeIndex索引之后的元素都往前移动完毕后清空最后一个元素
items[i] = null;
this.putIndex = i;
break;//结束循环
}
}
count--;//队列元素减1
if (itrs != null)
itrs.removedAt(removeIndex);//更新迭代器数据
}
notFull.signal();//唤醒添加线程
}
//从队列头部删除,队列没有元素就阻塞,可中断
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//中断
try {
//如果队列没有元素
while (count == 0)
//执行阻塞操作
notEmpty.await();
return dequeue();//如果队列有元素执行删除操作
} finally {
lock.unlock();
}
}
......
}
从上面的入队/出队操作,可以看出,ArrayBlockingQueue的内部数组其实是一种环形结构。
假设ArrayBlockingQueue的容量大小为6,我们来看下整个入队过程:
①初始时
②插入元素“9”
③插入元素“2”、“10”、“25”、“93”
④插入元素“90”
注意,此时再插入一个元素“90”,则putIndex变成6,等于队列容量6,由于是循环队列,所以会将tableIndex重置为0:
这是队列已经满了(count==6),如果再有线程尝试插入元素,并不会覆盖原有值,而是被阻塞。
我们再来看下出队过程:
①出队元素“9”
②出队元素“2”、“10”、“25”、“93”
③出队元素“90”
注意,此时再出队一个元素“90”,则tabeIndex变成6,等于队列容量6,由于是循环队列,所以会将tableIndex重置为0:
这是队列已经空了(count==0),如果再有线程尝试出队元素,则会被阻塞。
总结:ArrayBlockingQueue利用了ReentrantLock来保证线程的安全性,针对队列的修改都需要加全局锁。在一般的应用场景下已经足够。对于超高并发的环境,由于生产者-消息者共用一把锁,可能出现性能瓶颈。
参考:Java多线程进阶(三三)—— J.U.C之collections框架:LinkedBlockingQueue
一个由链表结构组成的双向阻塞队列。
入队和出队采用来个独立的锁来控制数据同步(生产者和消费者可以并行,能高效的处理并发数据),多线程并发时,可以将锁的竞争最多降到一半。
构造时最好指定容量大小,没有则默认一个类似无限大小的容量(Integer.MAX_VALUE)————存在风险:如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽
内部是使用链表实现一个队列的,但是却有别于一般的队列,在于该队列至少有一个节点,头节点不含有元素。
扩展:LinkedBlockingQueue和ArrayBlockingQueue比较主要有以下区别:
\1. 队列大小不同。ArrayBlockingQueue初始构造时必须指定大小,而LinkedBlockingQueue构造时既可以指定大小,也可以不指定(默认为Integer.MAX_VALUE,近似于无界);
\2. 底层数据结构不同。ArrayBlockingQueue底层采用数组作为数据存储容器,而LinkedBlockingQueue底层采用单链表作为数据存储容器;
\3. 两者的加锁机制不同。ArrayBlockingQueue使用一把全局锁,即入队和出队使用同一个ReentrantLock锁;而LinkedBlockingQueue进行了锁分离,入队使用一个ReentrantLock锁(putLock),出队使用另一个ReentrantLock锁(takeLock);
\4. LinkedBlockingQueue不能指定公平/非公平策略(默认都是非公平),而ArrayBlockingQueue可以指定策略。
\5. ArrayBlockingQueue在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于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
(null); } /** * 从已有集合构造队列. * 队列容量为Integer.MAX_VALUE */ public LinkedBlockingQueue(Collection extends E> c) { this(Integer.MAX_VALUE); final ReentrantLock putLock = this.putLock; putLock.lock(); // 这里加锁仅仅是为了保证可见性 try { int n = 0; for (E e : c) { if (e == null) // 队列不能包含null元素 throw new NullPointerException(); if (n == capacity) // 队列已满 throw new IllegalStateException("Queue full"); enqueue(new Node (e)); // 队尾插入元素 ++n; } count.set(n); // 设置元素个数 } finally { putLock.unlock(); } } //节点类,用于存储数据 static class Node { E item; Node next; Node(E x) { item = x; } } // 容量大小 private final int capacity; // 使用了一个原子变量AtomicInteger记录队列中元素的个数,以保证入队/出队并发修改元素时的数据一致性。,因为有2个锁,存在竞态条件,使用AtomicInteger private final AtomicInteger count = new AtomicInteger(); // 头结点 private transient Node head; // 尾节点 private transient Node last; // 获取并移除元素时使用的锁,如take, poll, etc private final ReentrantLock takeLock = new ReentrantLock(); // notEmpty条件对象,当队列没有数据时用于挂起执行删除的线程 private final Condition notEmpty = takeLock.newCondition(); // 添加元素时使用的锁如 put, offer, etc private final ReentrantLock putLock = new ReentrantLock(); // notFull条件对象,当队列数据已满时用于挂起执行添加的线程 private final Condition notFull = putLock.newCondition(); public LinkedBlockingQueue() { this(Integer.MAX_VALUE); } public LinkedBlockingQueue(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; last = head = new Node (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)); ++n; } count.set(n); } finally { putLock.unlock(); } } /** * 在队尾插入指定的元素. * 如果队列已满,则阻塞线程. */ 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上等待 notFull.await(); } enqueue(node); // 将新结点链接到“队尾” /** * c+1 表示的元素个数. * 如果,则唤醒一个“入队线程” */ c = count.getAndIncrement(); // c表示入队前的队列元素个数 if (c + 1 < capacity) // 入队后队列未满, 则唤醒一个“入队线程” notFull.signal(); } finally { putLock.unlock(); // 释放锁,下面可能会获得出队的锁,避免死锁 } //每入队一个元素,都要判断下队列是否空了,如果空了,说明可能存在正在等待的“出队线程”,需要唤醒它 if (c == 0) // 队列初始为空, 则唤醒一个“出队线程” signalNotEmpty(); } /** * 从队首出队一个元素 */ 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(); // c表示出队前的元素个数 if (c > 1) // 出队前队列非空, 则唤醒一个出队线程 notEmpty.signal(); } finally { takeLock.unlock(); } // 每入队一个元素,都要判断下队列是否满,如果是满的,说明可能存在正在等待的“入队线程”,需要唤醒它 if (c == capacity) // 队列初始为满,则唤醒一个入队线程 signalNotFull(); return x; } /** * 队首出队一个元素. */ private E dequeue() { Node h = head; Node first = h.next; h.next = h; // 原来的head指向自己,help GC head = first; E x = first.item; first.item = null; // 头结点置空 return x; }
优先级的判断通过构造函数传入的Compator对象来决定
不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者——生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间
内部控制线程同步的锁采用的是公平锁
public class PriorityBlockingQueue extends AbstractQueue
implements BlockingQueue, java.io.Serializable {
/**
* 默认容量.
*/
private static final int DEFAULT_INITIAL_CAPACITY = 11;
/**
* 最大容量.
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 内部堆数组, 保存实际数据, 可以看成一颗二叉树:
* 对于顶点queue[n], queue[2*n+1]表示左子结点, queue[2*(n+1)]表示右子结点.
*/
private transient Object[] queue;
/**
* 队列中的元素个数.
*/
private transient int size;
/**
* 比较器, 如果为null, 表示以元素自身的自然顺序进行比较(元素必须实现Comparable接口).
*/
private transient Comparator super E> comparator;
/**
* 全局锁.
*/
private final ReentrantLock lock;
/**
* 当队列为空时,出队线程在该条件队列上等待.
*/
private final Condition notEmpty;
public class PriorityBlockingQueue extends AbstractQueue
implements BlockingQueue, java.io.Serializable {
/**
* 默认容量.
*/
private static final int DEFAULT_INITIAL_CAPACITY = 11;
/**
* 最大容量.
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 内部堆数组, 保存实际数据, 可以看成一颗二叉树:
* 对于顶点queue[n], queue[2*n+1]表示左子结点, queue[2*(n+1)]表示右子结点.
*/
private transient Object[] queue;
/**
* 队列中的元素个数.
*/
private transient int size;
/**
* 比较器, 如果为null, 表示以元素自身的自然顺序进行比较(元素必须实现Comparable接口).
*/
private transient Comparator super E> comparator;
/**
* 全局锁.
*/
private final ReentrantLock lock;
/**
* 当队列为空时,出队线程在该条件队列上等待.
*/
private final Condition notEmpty;
/**
* 将元素x插入到array[k]的位置.
* 然后按照元素的自然顺序进行堆调整——"上浮",以维持"堆"有序.
* 最终的结果是一个"小顶堆".
*/
private static void siftUpComparable(int k, T x, Object[] array) {
Comparable super T> key = (Comparable super T>) x;
while (k > 0) {
int parent = (k - 1) >>> 1; // 相当于(k-1)除2, 就是求k结点的父结点索引parent
Object e = array[parent];
if (key.compareTo((T) e) >= 0) // 如果插入的结点值大于父结点, 则退出
break;
// 否则,交换父结点和当前结点的值
array[k] = e;
k = parent;
}
array[k] = key;
}
}
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); // 扩容和入队/出队可以同时进行, 所以先释放全局锁
Object[] newArray = null;
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) { // allocationSpinLock置1表示正在扩容
try {
// 计算新的数组大小
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) :
(oldCap >> 1));
if (newCap - MAX_ARRAY_SIZE > 0) { // 溢出判断
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) // 扩容失败(可能有其它线程正在扩容,导致allocationSpinLock竞争失败)
Thread.yield();
lock.lock(); // 获取全局锁(因为要修改内部数组queue)
if (newArray != null && queue == array) {
queue = newArray; // 指向新的内部数组
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
/**
* 出队一个元素.
* 如果队列为空, 则阻塞线程.
*/
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 获取全局锁
E result;
try {
while ((result = dequeue()) == null) // 队列为空
notEmpty.await(); // 线程在noEmpty条件队列等待
} finally {
lock.unlock();
}
return result;
}
private E dequeue() {
int n = size - 1; // n表示出队后的剩余元素个数
if (n < 0) // 队列为空, 则返回null
return null;
else {
Object[] array = queue;
E result = (E) array[0]; // array[0]是堆顶结点, 每次出队都删除堆顶结点
E x = (E) array[n]; // array[n]是堆的最后一个结点, 也就是二叉树的最右下结点
array[n] = null;
Comparator super E> cmp = comparator;
if (cmp == null)
siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;
return result;
}
}
/**
* 堆的"下沉"调整.
* 删除array[k]对应的结点,并重新调整堆使其有序.
*
* @param k 待删除的位置
* @param x 待比较的健
* @param array 堆数组
* @param n 堆的大小
*/
private static void siftDownComparable(int k, T x, Object[] array, int n) {
if (n > 0) {
Comparable super T> key = (Comparable super T>) x;
int half = n >>> 1; // 相当于n除2, 即找到索引n对应结点的父结点
while (k < half) {
/**
* 下述代码中:
* c保存k的左右子结点中的较小结点值
* child保存较小结点对应的索引
*/
int child = (k << 1) + 1; // k的左子结点
Object c = array[child];
int right = child + 1; // k的右子结点
if (right < n && ((Comparable super T>) c).compareTo((T) array[right]) > 0)
c = array[child = right];
if (key.compareTo((T) c) <= 0)
break;
array[k] = c;
k = child;
}
array[k] = key;
}
}
}
堆的“上浮”调整
我们通过示例来理解下入队的整个过程:假设初始构造的队列大小为6,依次插入9、2、93、10、25、90。
①初始队列情况
②插入元素9(索引0处)
③插入元素2(索引1处)
由于结点2的父结点为9,所以要进行“上浮调整”,最终队列结构如下:
④插入元素93(索引2处)
⑤插入元素10(索引3处)
⑥插入元素25(索引4处)
⑦插入元素90(索引5处)
此时,堆不满足有序条件,因为“90”的父结点“93”大于它,所以需要“上浮调整”:
最终,堆的结构如上,可以看到,经过调整后,堆顶元素一定是最小的。
堆的“下沉”调整
来看个示例,假设堆的初始结构如下,现在出队一个元素(索引0位置的元素2)。
①初始状态
对应二叉树结构:
②将顶点与最后一个结点调换
即将顶点“2”与最后一个结点“93”交换,然后将索引5为止置null。
注意: 为了提升效率(比如siftDownComparable的源码所示)并不一定要真正交换,可以用一个变量保存索引5处的结点值,在整个下沉操作完成后再替换。但是为了理解这一过程,示例图中全是以交换进行的。
③下沉索引0处结点
比较元素“93”和左右子结点中的最小者,发现“93”大于“9”,违反了“小顶堆”的规则,所以交换“93”和“9”,这一过程称为siftdown(下沉):
④继续下沉索引1处结点
比较元素“93”和左右子结点中的最小者,发现“93”大于“10”,违反了“小顶堆”的规则,所以交换“93”和“10”:
⑤比较结束
由于“93”已经没有左右子结点了,所以下沉结束,可以看到,此时堆恢复了有序状态,最终队列结构如下:
一个实现PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有当其指定的延迟时间到了,才能够从队列中获取到该元素
没有大小限制的队列
入队(生产者)不阻塞,出队(消费者)阻塞
DelayQueue内部使用非线程安全的优先队列(PriorityQueue),并使用Leader/Followers模式,最小化不必要的等待时间。
扩展:
Leader/Followers模式:
有若干个线程(一般组成线程池)用来处理大量的事件
有一个线程作为领导者,等待事件的发生;其他的线程作为追随者,仅仅是睡眠。
假如有事件需要处理,领导者会从追随者中指定一个新的领导者,自己去处理事件。
唤醒的追随者作为新的领导者等待事件的发生。
处理事件的线程处理完毕以后,就会成为追随者的一员,直到被唤醒成为领导者。
假如需要处理的事件太多,而线程数量不够(能够动态创建线程处理另当别论),则有的事件可能会得不到处理。
所有线程会有三种身份中的一种:leader和follower,以及一个干活中的状态:proccesser。它的基本原则就是,永远最多只有一个leader。而所有follower都在等待成为leader。线程池启动时会自动产生一个Leader负责等待网络IO事件,当有一个事件产生时,Leader线程首先通知一个Follower线程将其提拔为新的Leader,然后自己就去干活了,去处理这个网络事件,处理完毕后加入Follower线程等待队列,等待下次成为Leader。这种方法可以增强CPU高速缓存相似性,及消除动态内存分配和线程间的数据交换。
源码分析可参考:https://www.cnblogs.com/WangHaiMing/p/8798709.html
应用场景:
(1)使用一个DelayQueue来管理一个超时未响应的连接队列。
(2)缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
(3)定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。
一种无缓冲的等待队列,类似于无中介的直接交易。一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。
声明一个SynchronousQueue有公平模式和非公平模式两种不同的方式,它们之间有着不太一样的行为。
公平模式和非公平模式的区别:
SynchronousQueue根据公平/非公平访问策略的不同,内部使用了两种不同的数据结构:栈和队列。对于公平策略,内部构造了一个TransferQueue对象,而非公平策略则是构造了TransferStack对象。这两个类都继承了内部类Transferer,SynchronousQueue中的所有方法,其实都是委托调用了TransferQueue/TransferStack的方法
非公平模式:非公平策略由TransferStack类实现,既然TransferStack是栈,那就有结点。TransferStack内部定义了名为SNode的结点:
static final class SNode { volatile SNode next; volatile SNode match; // 与当前结点配对的结点 volatile Thread waiter; // 当前结点对应的线程 Object item; // 实际数据或null int mode; // 结点类型 SNode(Object item) { this.item = item; } // Unsafe mechanics private static final sun.misc.Unsafe UNSAFE; private static final long matchOffset; private static final long nextOffset; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class> k = SNode.class; matchOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("match")); nextOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("next")); } catch (Exception e) { throw new Error(e); } } // ... } static final class TransferStack
extends Transferer { /** * 未配对的消费者 */ static final int REQUEST = 0; /** * 未配对的生产者 */ static final int DATA = 1; /** * 配对成功的消费者/生产者 */ static final int FULFILLING = 2; volatile SNode head; // Unsafe mechanics private static final sun.misc.Unsafe UNSAFE; private static final long headOffset; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class> k = TransferStack.class; headOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("head")); } catch (Exception e) { throw new Error(e); } } // ... } 上述SNode结点的定义中有个
mode
字段,表示结点的类型。TransferStack一共定义了三种结点类型,任何线程对TransferStack的操作都会创建下述三种类型的某种结点:
- REQUEST:表示未配对的消费者(当线程进行出队操作时,会创建一个mode值为REQUEST的SNode结点 )
- DATA:表示未配对的生产者(当线程进行入队操作时,会创建一个mode值为DATA的SNode结点 )
- FULFILLING:表示配对成功的消费者/生产者
核心操作——put/take
/** * 入队指定元素e. * 如果没有另一个线程进行出队操作, 则阻塞该入队线程. */ 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(); } } /** * 出队一个元素. * 如果没有另一个线程进行入队操作, 则阻塞该出队线程. */ public E take() throws InterruptedException { E e = transferer.transfer(null, false, 0); if (e != null) return e; Thread.interrupted(); throw new InterruptedException(); } /** * 入队/出队一个元素. * SynchronousQueue一样不支持null元素,实际的入队/出队操作都是委托给了transfer方法,该方法返回null表示出/入队失败(通常是线程被中断或超时) */ E transfer(E e, boolean timed, long nanos) { SNode s = null; // s表示新创建的结点 // 入参e==null, 说明当前是出队线程(消费者), 否则是入队线程(生产者) // 入队线程创建一个DATA结点, 出队线程创建一个REQUEST结点 int mode = (e == null) ? REQUEST : DATA; for (; ; ) { // 自旋 SNode h = head; if (h == null || h.mode == mode) { // CASE1: 栈为空 或 栈顶结点类型与当前mode相同 if (timed && nanos <= 0) { // case1.1: 限时等待的情况 if (h != null && h.isCancelled()) casHead(h, h.next); else return null; } else if (casHead(h, s = snode(s, e, h, mode))) { // case1.2 将当前结点压入栈 SNode m = awaitFulfill(s, timed, nanos); // 阻塞当前调用线程 if (m == s) { // 阻塞过程中被中断 clean(s); return null; } // 此时m为配对结点 if ((h = head) != null && h.next == s) casHead(h, s.next); // 入队线程null, 出队线程返回配对结点的值 return (E) ((mode == REQUEST) ? m.item : s.item); } // 执行到此处说明入栈失败(多个线程同时入栈导致CAS操作head失败),则进入下一次自旋继续执行 } else if (!isFulfilling(h.mode)) { // CASE2: 栈顶结点还未配对成功 if (h.isCancelled()) // case2.1: 元素取消情况(因中断或超时)的处理 casHead(h, h.next); else if (casHead(h, s = snode(s, e, h, FULFILLING | mode))) { // case2.2: 将当前结点压入栈中 for (; ; ) { SNode m = s.next; // s.next指向原栈顶结点(也就是与当前结点匹配的结点) if (m == null) { // m==null说明被其它线程抢先匹配了, 则跳出循环, 重新下一次自旋 casHead(s, null); s = null; break; } SNode mn = m.next; if (m.tryMatch(s)) { // 进行结点匹配 casHead(s, mn); // 匹配成功, 将匹配的两个结点全部弹出栈 return (E) ((mode == REQUEST) ? m.item : s.item); // 返回匹配值 } else // 匹配失败 s.casNext(m, mn); // 移除原待匹配结点 } } } else { // CASE3: 其它线程正在匹配 SNode m = h.next; if (m == null) // 栈顶的next==null, 则直接弹出, 重新进入下一次自旋 casHead(h, null); else { // 尝试和其它线程竞争匹配 SNode mn = m.next; if (m.tryMatch(h)) casHead(h, mn); // 匹配成功 else h.casNext(m, mn); // 匹配失败(被其它线程抢先匹配成功了) } } } }
整个transfer方法考虑了限时等待的情况,且入队/出队其实都是调用了同一个方法,其主干逻辑就是在一个自旋中完成以下三种情况之一的操作,直到成功,或者被中断或超时取消:
- 栈为空,或栈顶结点类型与当前入队结点相同。这种情况,调用线程会阻塞;
- 栈顶结点还未配对成功,且与当前入队结点可以配对。这种情况,直接进行配对操作;
- 栈顶结点正在配对中。这种情况,直接进行下一个结点的配对。
应用场景:
SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。
(以下内容大部分参考ThreadPoolExecutor线程池解析与BlockingQueue的三种实现)
据了解,在 线程池,future,futuretask,runnable,callable等地方都运用到了阻塞队列。
扩展:在Java5之前,线程是没有返回值的,可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。
下面以 线程池 为例做针对性分析:
线程池:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler){...}
TimeUnit:时间单位;BlockingQueue:等待的线程存放队列;keepAliveTime:非核心线程的闲置超时时间,超过这个时间就会被回收;RejectedExecutionHandler:线程池对拒绝任务的处理策略。 自定义线程池:这个构造方法对于队列是什么类型比较关键。队列在线程池中是非常重要的角色,那么Executors就是根据不同的队列实现了功能不同的线程池。
线程池工作方式:
在使用有界队列时,若有新的任务需要执行,如果线程池实际线程数小于corePoolSize,则优先创建线程,
若大于corePoolSize,则会将任务加入队列,
若队列已满,则在总线程数不大于maximumPoolSize的前提下,创建新的线程,
若队列已经满了且线程数大于maximumPoolSize,则执行拒绝策略。或其他自定义方式。
来源:Executors包含的常用线程池
1.ExecutorService newFixedThreadPool(int nThreads)
:固定大小线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
coresize和maxsize相同,超时时间为0,队列用的LinkedBlockingQueue无界的FIFO队列,这表示什么,很明显,这个线程池始终只有
2.ExecutorService newCachedThreadPool()
:无界线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
SynchronousQueue队列,一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作。所以,当我们提交第一个任务的时候,是加入不了队列的,这就满足了,一个线程池条件“当无法加入队列的时候,且任务没有达到maxsize时,我们将新开启一个线程任务”。所以我们的maxsize是big big。时间是60s,当一个线程没有任务执行会暂时保存60s超时时间,如果没有的新的任务的话,会从cache中remove掉
3.Executors.newSingleThreadExecutor()
;大小为1的固定线程池,这个其实就是newFixedThreadPool(1).关注newFixedThreadPool的用法就行
(因个人能力,时间精力等原因,仍有不足,后续将继续完善。。。)
结束和声明
以上纯属个人观点和体会,相关的资料和观点来自网络的朋友们! 希望这篇文章能对你有所帮助! 欢迎大家来一起讨论分享干货,或者批评指正! 更加热切盼望各路大神前辈给些指导和建议! 转载请注明出处!或者联系我!([email protected])