当前位置: 代码迷 >> 综合 >> 并发编程(7):阻塞队列BlockingQueue的基本使用与实现原理
  详细解决方案

并发编程(7):阻塞队列BlockingQueue的基本使用与实现原理

热度:55   发布时间:2023-10-24 16:01:53.0

1、队列Queue是一种数据结构,满足FIFO即先进先出的原则,Java中Queue 和 List 、Set 一样都继承自 Collection 接口,其中我们经常用到的 LinkedList 实现了 Queue 接口。

2、而在并发队列上, JDK 提供了两套实现:一个就是以 ConcurrentLinkedQueue 为代表的高性能的非阻塞队列,一个是以 BlockingQueue 为代表的阻塞队列。’例如:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue等。这两种都实现或继承自 Queue。

 

3、ConcurrentLinkedQueue、ArrayBlockingQueue的基本是方法使用:

      ConcurrentLinkedQueue案例:

/*** Java 队列基本操作----->非阻塞队列*/
public class ConcurrentLinkedQueueDemo {//无界线程安全队列,不允许存入null元素static Queue<String> queue = new ConcurrentLinkedQueue<>();public static void main(String[] args) {queue.add("1");queue.add("2");queue.add("3");queue.add("4");queue.add("5");             //向队列中添加一个元素,添加成功返回true  容量不足抛出IllegalStateExceptionqueue.offer("6");           //向队列中添加一个元素,添加成功返回true  容量不足返回falseSystem.out.println(queue);queue.remove();              //移除队首元素,队列为空的时候抛出NoSuchElementExceptionqueue.poll();                //移除队首元素,队列为空的时候返回nullSystem.out.println(queue);String element = queue.element();     //查看队首元素,队列为空时抛NoSuchElementExceptionString peek = queue.peek();           //查看队首元素,队列为空时返回nullSystem.out.println(element);System.out.println(peek);}
}

            主要的方法说明:

              boolean add(E e)         //向队列中添加一个元素,如果队列满了将抛出llegalStateException
              boolean offer(E e)        //向队列中添加一个元素,添加成功返回true  容量不足返回false
              E remove()                   //移除队首元素,队列为空的时候抛出NoSuchElementException
              E poll()                         //移除队首元素,队列为空的时候返回null
              E element()                  //查看队首元素,队列为空时抛NoSuchElementException
              E peek()                       //查看队首元素,队列为空时返回null

 

      ArrayBlockingQueue案例:

/*** Java 队列基本操作----->阻塞队列*/
public class BlockingQueueDemo {//由数组支持的有界队列static ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(5);public static void main(String[] args) throws InterruptedException {queue.add(1);queue.add(2);queue.add(3);queue.add(4);queue.add(5);                                  //向队列中添加一个元素,添加成功返回true  容量不足抛出IllegalStateExceptionqueue.put(6);                                  //在队列满了的情况下使用 阻塞队列的put()方法将会阻塞当前线程。boolean offer = queue.offer(6);                //向队列中添加一个元素,添加成功返回true  容量不足返回falsequeue.offer(6,2, TimeUnit.SECONDS);            //在队列满了的情况下使用 阻塞队列的offer(E e, long timeout, TimeUnit unit)方法将会阻塞当前线程指定时间。queue.remove();                                    //移除队首元素,队列为空的时候抛出NoSuchElementExceptionqueue.take();                                      //移除队首元素,队列为空时阻塞当前线程queue.poll();                                      //移除队首元素,队列为空的时候返回nullqueue.poll(2, TimeUnit.SECONDS);                   //移除队首元素,队列为空时当前线程阻塞指定时间System.out.println(queue);Integer element = queue.element();     //查看队首元素,队列为时抛NoSuchElementExceptionInteger peek = queue.peek();           //查看队首元素,队列为时返回nullSystem.out.println(element);System.out.println(peek);}
}

 

            主要的方法说明:

              boolean add(E e)         //向队列中添加一个元素,如果队列满了将抛出llegalStateException。
              boolean offer(E e)        //向队列中添加一个元素,添加成功返回true  容量不足返回false。
              E remove()                   //移除队首元素,队列为空的时候抛出NoSuchElementException。
              E poll()                         //移除队首元素,队列为空的时候返回null。
              E element()                  //查看队首元素,队列为空时抛NoSuchElementException。
              E peek()                       //查看队首元素,队列为空时返回null。

            除了上面的通用的方法还有特性的方法:

              put(E e)                                                                   //在队列满了的情况下使用 阻塞队列的put()方法将会阻塞当前线程。
              boolean offer(E e, long timeout, TimeUnit unit)       //在队列满了的情况下使用 将会阻塞当前线程指定时间。
              E take()                                                                    //移除队首元素,队列为空时阻塞当前线程。
              E poll(long timeout, TimeUnit unit)                          //移除队首元素,队列为空时当前线程阻塞指定时间。

 

4、ArrayBlockingQueue的实现原理:

     先铺垫一下,阻塞队列的核心方法将就是特性的4个方法:

              put(E e)                                                                   //在队列满了的情况下使用 阻塞队列的put()方法将会阻塞当前线程。
              boolean offer(E e, long timeout, TimeUnit unit)       //在队列满了的情况下使用 将会阻塞当前线程指定时间。
              E take()                                                                    //移除队首元素,队列为空时阻塞当前线程。
              E poll(long timeout, TimeUnit unit)                          //移除队首元素,队列为空时当前线程阻塞指定时间。

             上面4 个方法会造成操作队列的线程阻塞,我们在介绍Condition的实现原理的时候我们自己也实现了一个阻塞队列MyBlockingQueue,我们实现的方式是使用ReentrantLock + Condition 来实现的。ReentrantLock我们知道是一把可重如锁,它是使用AQS同步器来实现线程排队获取锁。Condition呢是类似于Java 中的wait/notify 线程间通讯的功能,它的使用时必须使用Lock来创建一个Condition实例,其实现的原理是AQS同步器的sync同步队列跟Condition的等待队列来做线程节点的流转控制来实现的。!!!!!问题来了,我们要讲的阻塞队列ArrayBlockingQueue难道也是跟我们实现的MyBlockingQueue的的方式是一样的吗?这个问题有待验证。 接下来我们就一一验证我们的猜想。

              第一步我们先创建一个ArrayBlockingQueue实例:

                         ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(5);

                         构造函数的源码:

    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);//两个ConditionnotEmpty = lock.newCondition();notFull =  lock.newCondition();}

                  看完构造函数的源码我们心中就有了一点信心证明我们的猜想是对的,别急还有待验证。接下来我们看看put(E e)的实现,其源码:

    public void put(E e) throws InterruptedException {checkNotNull(e);//加锁final ReentrantLock lock = this.lock;lock.lockInterruptibly();try {while (count == items.length)//如果队列满了,那就当前线程假如到notFull 的Condition的等待队列中。notFull.await();//如果队列没满,那就入队。enqueue(e);} finally {//释放锁lock.unlock();}}//接上面的入队方法private void enqueue(E x) {// assert lock.getHoldCount() == 1;// assert items[putIndex] == null;final Object[] items = this.items;items[putIndex] = x;if (++putIndex == items.length)putIndex = 0;count++;//队列中有元素了,唤醒在队列为空的时候来获取元素的线程,待会我们会查看,当队列为空的时候,有线程来去元素实在什么地方被阻塞的。notEmpty.signal();}

                   看完put(E e)方法的实现,我们已经确定了,ArrayBlockingQueue难道也是跟我们实现的MyBlockingQueue的方式是一模一样的,使用ReenreantLock+Condition来实现线程的阻塞与唤醒的。

                   为了保险起见我们还是去看看E take() 方法的实现,其源码:

    public E take() throws InterruptedException {final ReentrantLock lock = this.lock;/*抢占锁,如果没抢到锁被阻塞,然后在被唤醒的实现,如果是被中断的直接出InterruptedException,区别于之前说的acquireQueued(final Node node, int arg)方法,因为acquireQueued方法当阻塞的线程被唤醒后,如果是被中断的会自我中断。*/lock.lockInterruptibly();try {while (count == 0)//如果来获取队列中的数据,发现队列是空的那就将当前线程添加到notEmpty的Condition等待队列中。notEmpty.await();//如果队列不为空,那就移除首元素。return dequeue();} finally {//释放锁lock.unlock();}}//接上面的移除队列首元素private E dequeue() {// assert lock.getHoldCount() == 1;// assert items[takeIndex] != null;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的Condition等待队列中的线程。notFull.signal();return x;}

                看完E take() 方法我们的猜想被完全证实了是对的,确实阻塞队列是使用ReentrantLock + 两个Condition来实现的。