阻塞队列
非阻塞队列是一个先进先出的单向队列(Queue),而BlockingQueue阻塞队列实际是非阻塞队列的一个加强,之所以是阻塞队列最主要的是实现take和put,当阻塞队列满时,put会一直阻塞直到拿到数据,或者响应中断退出;当队列空时,take元素,队列也会阻塞直到队列可用。
阻塞的意思就是此条线程会处于等待卡死状态,解锁的条件是队列中有另一条线程存入或取出数据了,就会解锁,就相当于队列是仓库,仓库没有货了就生产,有货就能消费,锁条件是notFull和notEmpty。
Throws exception | Special value | Blocks | Times out | |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除方法 | remove() | poll() | take() | poll(time, unit) |
检查方法 | element() | peek() |
队列(Queue和Deque)
队列是一种特殊的线性表,链表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。
进行插入操作的端称为队尾,进行删除操作的端称为队头。
单向队列(Queue):先进先出(FIFO),只能从队列尾插入数据,只能从队列头删除数据.
双向队列(Deque):可以从队列尾/头插入数据,只能从队列头/尾删除数据.操作头和尾.
单向队列Queue结构
private Node first;
private Node last;
int size = 0;
class Node{
private Node next;
private Object element;
public Node(Object element) {
this.element = element;
}
public Node(){}
}
单向队列操作
public void push(Object element){
//单向队列(Queue):先进先出(FIFO),只能从队列尾插入数据
size++;
Node node = new Node(element);
if (size>1){
this.last.next = node;
this.last = node;
}else if (size == 1){
this.first = node;
this.last = node;
}
}
public void pull(){
//单向队列(Queue):先进先出(FIFO),只能从队列头删除数据.
size--;
this.first = this.first.next;
if (size == 1){
this.last = this.first;
}
}
常见的阻塞队列
阻塞队列:BlockingQueue,多用于创建线程池
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列,只维护一个lock。
线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在corePoolSize以下,反之当任务队列已满时,则会以maximumPoolSize为最大线程数上限。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列,维护两个lock。
如果不设置队列容量,其初始化的默认容量大到可以认为是无界队列了,在这种情况下maximumPoolSize参数是无效的,队列中的任务太多,以至于由于无法及时处理导致一直增长,直到最后资源耗尽的问题。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayedWorkQueue:ScheduledThreadPoolExecutor静态内部类的一个无界延迟阻塞队列,添加到队列中的任务,会按照任务的延时时间进行排序,延时时间少的任务首先被执行
SynchronousQueue:无存储空间的阻塞队列。
1.SynchronousQueue是一个双栈双队列算法,无空间的队列,每执行一个插入操作就会阻塞,需要再执行一个删除操作才会被唤醒,反之每一个删除操作也都要等待对应的插入操作。
2.提交的任务不会被保存,总是会马上提交执行。如果用于执行任务的线程数量小于maximumPoolSize,则尝试创建新的进程,如果达到maximumPoolSize设置的最大值,则根据设置的handler执行拒绝策略。因此这种方式提交的任务不会被缓存起来,而是会被马上执行,在这种情况下,需要对程序的并发量有个准确的评估,才能设置合适的maximumPoolSize数量,否则很容易就会执行拒绝策略;
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
非阻塞队列的API
除了阻塞队列就是非阻塞队列,非阻塞队列,就是单纯意义上的"非"阻塞队列,其没有put和take方法,可以无限制存,且是线程安全的,N个用户同时存也能保证每次存放在队尾而不乱掉,但是其的size()方法会不定时遍历,所以很耗时
以ConcurrentLinkedQueue并发队列为例,多用于消息队列,并发异步处理,如日志等;add和poll是线程安全的,但是其他方法并没有保证线程安全,如判断isEmpty(),所以在多线程并发时还得自己加锁
ConcurrentLinkedQueue内部就是一个简单的单链表结构,每入队一个元素就是插入一个Node类型的结点。head指向队列头,tail指向队列尾,通过Unsafe来CAS操作字段值以及Node对象的字段值
offer/add添加队列元素:两个方法一样,都是将指定元素插入此队列的尾部,添加成功返回true;区别在于队列满时,add会抛异常,而offer会返回false
poll获取并移除此队列的头,如果此队列为空,则返回 null
public static void main(String[] args) {
ConcurrentLinkedQueue<Object> queue = new ConcurrentLinkedQueue<>();
queue.add("1000");
queue.offer("2000");
//先进先出
System.out.println(queue.poll());//1000
System.out.println(queue.poll());//2000
System.out.println(queue.poll());//null
}
peek获取但不移出队列的头,如果此队列为空,则返回 null
public static void main(String[] args) {
ConcurrentLinkedQueue<Object> queue = new ConcurrentLinkedQueue<>();
queue.add("1000");
queue.offer("2000");
//peek获取但不移除队列的头,所以每次peek()都是1000
System.out.println(queue.peek());//1000
System.out.println(queue.peek());//1000
System.out.println(queue.poll());//1000
}
remove从队列中移除指定元素,已存在元素,会返回true,remove不存在元素,返回false
contains判断当前队列是否包含指定元素,如果存在返回true,不存在返回false
public static void main(String[] args) {
ConcurrentLinkedQueue<Object> queue = new ConcurrentLinkedQueue<>();
queue.add("1000");
queue.offer("2000");
System.out.println(queue.remove("1000"));//true
//1000已经被移除了
System.out.println(queue.remove("1000"));//false
System.out.println(queue.contains("1000"));//false
System.out.println(queue.contains("2000"));//true
}
size队列的数量,如果此队列包含的元素数大于 Integer.MAX_VALUE,则只会返回 Integer.MAX_VALUE。由于这些队列的异步特性,确定当前的元素数需要进行一次花费 O(n) 时间的遍历。所以在需要判断队列是否为空时,使用peek()!=null,不要用 queue.size()>0
isEmpty判断当前队列是否为null
**toArray(T[] a)**转为指定数组,按照头->尾的顺序返回
ConcurrentLinkedQueue<Object> queue = new ConcurrentLinkedQueue<>();
queue.add("1000");
queue.offer("2000");
queue.offer("3000");
Object[] array = queue.toArray();
for (Object o : array){
System.out.println(o);1000->2000->3000
}
iterator获取按照头到尾的顺序遍历的迭代器
ConcurrentLinkedQueue<Object> queue = new ConcurrentLinkedQueue<>();
queue.add("1000");
queue.offer("2000");
Iterator<Object> iterator = queue.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
队列应用的示例:
//创建并发队列
ConcurrentLinkedQueue<Object> queue = new ConcurrentLinkedQueue<>();
//提供100000份需要消耗的食物,并放入队列
int productionNum = 10000;
for(int i = 1;i<10001;i++){
queue.offer("食物编号:"+i);
}
ThreadPoolExecutor executorService = (ThreadPoolExecutor)Executors.newFixedThreadPool(100);
for (int i =0;i<10;i++){
//submit()有返回值,而execute()没有
Future<?> future = executorService.submit(new Runnable() {
@Override
public void run() {
while (!queue.isEmpty()) {
System.out.println("当前线程为:" + Thread.currentThread().getName() + ":" + queue.poll());
}
}
});
}
ArrayBlockQueue/LinkedBlockQueue
ArrayBlockingQueue基于数组不会产生或销毁任何额外的对象实例,LinkedBlockingQueue基于链表会生成额外的Node对象会有会内存溢出的风险。但是常用的其实还是LinkedBlockingQueue,使用两套锁,实现生产和消费操作的并行,单个锁只能保证生产者和消费者只能每次操作一次生产或者消费,而双锁可以使得生产者在队列满时不断的向队列尾部添加node,消费者不断从head获取Node,从而吞吐效率更高
//Array可以选择公平锁和非公平锁,而Linked两把锁都是非公平锁
BlockingQueue<String> arrayQueue = new ArrayBlockingQueue<>(100,true);
BlockingQueue<String> linkedQueue = new LinkedBlockingQueue<>(100);
//阻塞的方法只有take和put
queue.take();
queue.put("");
ArrayBlockingQueue源码:
//数组
final Object[] items;
//获取数据的索引,主要用于take,poll,peek,remove方法
int takeIndex;
//添加数据的索引,主要用于 put, offer, or add 方法
int putIndex;
//队列元素的个数
int count;
final ReentrantLock lock;
//notEmpty条件对象,用于通知take方法队列已有元素,可执行获取操作
private final Condition notEmpty;
//notFull条件对象,用于通知put方法队列未满,可执行添加操作
private final Condition notFull;
put方法
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
//可中断加锁
lock.lockInterruptibly();
try {
//数组满了,生产条件释放锁并阻塞线程
while (count == items.length)
notFull.await();
//入队操作
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
final Object[] items = this.items;
//设置当前索引值为put添加的值,初始为0
items[putIndex] = x;
//length是固定的,简单理解就是当条件这个元素之后数组就满了
//那么当消费者从0开始往后消费,生产者被唤醒从而继续从0开始继续添加
//需要先了解消费的方式:会从头一直消费到最后一个元素之后又从0开始继续消费
if (++putIndex == items.length)
//消费者从0往后依次消费,生产者在从0开始继续添加
putIndex = 0;
count++;
//唤醒消费者
notEmpty.signal();
}
take方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//数组没有资源,消费者进入休眠
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
final Object[] items = this.items;
//takeIndex初始也为0,从索引0开始消费
E x = (E) items[takeIndex];
items[takeIndex] = null;
//take索引自增,当消费完最大的索引值,又从0开始消费
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
//消费之后数组未满,所以唤醒生产者继续生产
notFull.signal();
return x;
}
LinkedBlockingQueue源码:
//由Node单向链表组成的队列,初始化时创建一个空Node并且设置为head和last
static class Node<E> {
E item;
Node<E> next;
Node(E x) {
item = x; }
}
//队列容量
private final int capacity;
//队列长度,因为多个线程可以同时生产或消费,需要保证改变值的可见性和原子性
private final AtomicInteger count = new AtomicInteger();
//队列的头和尾
transient Node<E> head;
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();
put方法,可以实现入列和消费同时进行,但是生产或者消费时只能同时运行一个线程
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