阻塞队列是什么?
首先了解队列,队列是数据先进先出的一种数据结构。阻塞队列,关键字是阻塞,先理解阻塞的含义,在阻塞队列中,线程阻塞有这样的两种情况:
1.当阻塞队列为空时,获取队列元素的线程将等待,直到该则塞队列非空;
2.当阻塞队列变满时,使用该阻塞队列的线程会等待,直到该阻塞队列变成非满。
为什么要使用阻塞队列?
在常见的情况下,生产者消费者模式需要用到队列,生产者线程生产数据,放进队列,然后消费从队列中获取数据,这个在单线程的情况下没有问题。但是当多线程的情况下,某个特定时间下,(峰值高并发)出现消费者速度远大于生产者速度,消费者必须阻塞来等待生产者,以保证生产者能够生产出新的数据;当生产者速度远大于消费者速度时,同样也是一个道理。这些情况都要程序员自己控制阻塞,同时又要线程安全和运行效率。
阻塞队列的出现使得程序员不需要关心这些细节,比如什么时候阻塞线程,什么时候唤醒线程,这些都由阻塞队列完成了。
阻塞队列的主要方法
阻塞队列的方法,在不能立即满足但可能在将来某一时刻满足的情况下,按处理方式可以分为三类:
抛出异常:抛出一个异常;
特殊值:返回一个特殊值(null或false,视情况而定)
则塞:在成功操作之前,一直阻塞线程(推荐采用 put take)
超时:放弃前只在最大的时间内阻塞
工欲善其事必先利其器,学会用阻塞队列,必须要知道它有哪些方法,怎么用,有哪些注意事项,这样到真正使用的时候,就能少踩雷了。
首先介绍插入操作:
1.public abstract boolean add(E paramE);
将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则抛出IllegalStateException。
如果该元素是NULL,则会抛出NullPointerException异常。
2.public abstract boolean offer(E paramE);
将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则返回 false。
3.public abstract void put(E paramE) throws InterruptedException;
将指定元素插入此队列中,将等待可用的空间(如果有必要)
4.offer(E o, long timeout, TimeUnit unit)
可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
获取数据操作:
1.poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
2.poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间
超时还没有数据可取,返回失败。
3.take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;
4.drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。
常见的阻塞队列
首先来看这张图,这个是阻塞队列的继承图(双端队列,没有列出来,没有太大区别)
主要有ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue,SynchronousQueue,DelayQueue这个五个实现类。
在这五个阻塞队列中,比较常用的是ArrayBlockingQueue,LinkedBlockingQueue,本文也会重点介绍这两个类。
ArrayBlockingQueue
在上面的源码分析中就是分析的ArrayBlockingQueue的源码。数组阻塞队列必须传入的参数是数组大小,还可以指定是否公平性。公平性就是当队列可用时,线程访问队列的顺序按照它排队时候的顺序,非公平锁则不按照这样的顺序,但是非公平队列要比公平队列执行的速度快。
继续看ArrayBlockingQueue其实是一个数组有界队列,此队列按照先进先出的原则维护数组中的元素顺序,看源码可知,是由两个整形变量(上文提到的putIndex和takeIndex)分别指着头和尾的位置。
LinkedBlockingQueue
LinkedBlockingQueue是基于链表的阻塞队列,内部维持的数据缓冲队列是由链表组成的,也是按照先进先出的原则。
如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小(Integer.Max_VALUE)的容量,这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已经被消耗殆尽了。
LinkedBlockingQueue之所以能够高效的处理并发数据,是因为take()方法和put(E param)方法使用了不同的可重入锁,分别为private final ReentrantLock putLock和private final ReentrantLock takeLock,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
两者对比
1.ArrayBlockingQueue在put,take操作使用了同一个锁,两者操作不能同时进行,而LinkedBlockingQueue使用了不同的锁,put操作和take操作可同时进行。
2.ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象,这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。
其他还有优先级阻塞队列:PriorityBlockingQueue,延时队列:DelayQueue,SynchronousQueue等,因为使用频率较低,这里就不重点介绍了,有兴趣的读者可以深入研究。
用阻塞队列实现生产者消费者
使用阻塞队列代码要简单得多,不需要再单独考虑同步和线程间通信的问题
在并发编程中,一般推荐使用阻塞队列
public class BolckQuene_Pro_Con { private int queueSize = 10;//队列允许存放的最大数 private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(queueSize);//阻塞队列 public static void main(String[] args) { BolckQuene_Pro_Con test = new BolckQuene_Pro_Con(); Producer producer = test.new Producer(); Consumer consumer = test.new Consumer(); producer.start(); consumer.start(); } class Consumer extends Thread{ @Override public void run() { consume(); } private void consume() { while(true){ try { Thread.sleep(300); queue.take(); System.out.println("【消费者】从队列取走一个元素,队列剩余"+queue.size()+"个元素"); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Producer extends Thread{ @Override public void run() { produce(); } private void produce() { while(true){ try { Thread.sleep(200); queue.put(1); System.out.println("【生产者】向队列取中插入一个元素,队列中元素:"+queue.size()); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
简易版(如若没有消费将会产生阻塞一直等待,直到非满)
package thread; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestCountDownLatch { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);//阻塞队列 executorService.execute(() -> { while (true) { try { Thread.sleep(60000000); queue.take(); System.out.println("【消费者】从队列取走一个元素,队列剩余" + queue.size() + "个元素"); } catch (InterruptedException e) { e.printStackTrace(); } } }); executorService.execute(() -> { while (true) { try { Thread.sleep(200); queue.put(1); System.out.println("【生产者】向队列取中插入一个元素,队列中元素:" + queue.size()); } catch (InterruptedException e) { e.printStackTrace(); } } }); System.out.println("这个结束了"); } }
总的来说生产者的速度是会大于消费者的速度的,但是因为阻塞队列的缘故,所以我们不需要控制阻塞,当阻塞队列满的时候,生产者线程就会被阻塞,直到不再满。反之亦然,当消费者线程多于生产者线程时,消费者速度大于生产者速度,当队列为空时,就会阻塞消费者线程,直到队列非空。