BlockingQueue 理论

BlockingQueue 简述

  • 阻塞队列 (BlockingQueue)是Java.util.concurrent包下重要的数据结构,经常与CountDownLatch、CyclicBarrier、Semaphore这些一起谈论,可以参考《CountDownLatch 与 CyclicBarrier》
  • BlockingQueue 提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。
  • 并发包下很多高级同步类的实现都是基于BlockingQueue实现的。
  • public interface BlockingQueue<E>extends Queue<E>
  • BlockingQueue 不接受 null 元素。试图 add、put 或 offer 一个 null 元素时,某些实现会抛出 NullPointerException。null 被用作指示 poll 操作失败的警戒值。 
  • BlockingQueue 可以是限定容量的,没有容量约束时,如LinkedBlockingDeque<E>、LinkedBlockingQueue<E>默认 大小为Integer.MAX_VALUE
  • BlockingQueue 实现是线程安全的。
  • BlockingQueue典型应用就是生产者-消费者使用场景,BlockingQueue 可以安全地与多个生产者和多个使用者一起使用。 

四种操作形式

  • BlockingQueue 的方法以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同,如下表所示:
操作 抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
检查 element() peek() 不可用 不可用
  1. 第一种:如果试图的操作无法立即执行,抛出异常
  2. 第二种:如果试图的操作无法立即执行,返回一个特定的值(true / false /null取决于具体操作)。
  3. 第三种:如果试图的操作无法立即执行,该方法调用将会无限期阻塞,直到条件满足后再继续执行
  4. 第四种:如果试图的操作无法立即执行,该方法调用将会在指定时间内阻塞,直到条件满足后再继续执行或者超时后抛出异常

BlockingQueue实现类

  • BlockingQueue是接口,需要依靠它的实现BlockingQueue使用,主要实现类如下。

ArrayBlockingQueue

  • public class ArrayBlockingQueue<E>extends AbstractQueue<E>implements BlockingQueue<E>, Serializable
  • 一个由数组支持的有界阻塞队列,可以在对其初始化的时候设定这个有界上限,设定之后无法修改。
  • 数组阻塞队列按 FIFO(先进先出)原则对元素进行排序。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。 

DelayQueue

  • public class DelayQueue<E extends Delayed>extends AbstractQueue<E>implements BlockingQueue<E>
  • 延迟阻塞队列(DelayQueue)对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。
  • 实际使用的少

LinkedBlockingQueue

  • public class LinkedBlockingQueue<E>extends AbstractQueue<E>implements BlockingQueue<E>, Serializable
  • 一个基于已链接节点的、范围任意的链式阻塞队列
  • 此队列按 FIFO(先进先出)排序元素,新元素插入到队列的尾部,并且队列获取操作会获得位于队列头部的元素
  • 链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。 
  • 可选的容量范围构造方法参数作为防止队列过度扩展的一种方法。如果未指定容量,则它等于 Integer.MAX_VALUE。如果已经指定容量,则容量不会再动态改变

PriorityBlockingQueue

  • public class PriorityBlockingQueue<E>extends AbstractQueue<E>implements BlockingQueue<E>, Serializable
  • PriorityBlockingQueue 是一个无界阻塞队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。
  • 无法向这个队列中插入 null 值。

SynchronousQueue

  • public class SynchronousQueue<E>extends AbstractQueue<E>implements BlockingQueue<E>, Serializable
  • SynchronousQueue 同步阻塞队列,每次插入操作时必须同时有另一个线程在做对应的移除操作 ,否则插入操作会一直阻塞;反之,做移除操作时,必须有另一个线程在做插入操作,否则移除操作会一直阻塞。这样达到完全同步的效果!
  • 同步队列没有任何内部容量,是一个空 collection,所以插入与移除操作只能同时进行,否则只做插入或者只做移除时,此操作就会一直阻塞。
  • 此队列不允许 null 元素
  • 同步队列非常适合于传递性设计,在这种设计中,在一个线程中运行的对象要将某些信息、事件或任务传递给在另一个线程中运行的对象,它就必须与该对象同步。 

生产者-消费者

/**
 * Created by Administrator on 2018/6/14 0014.
 * 生产者
 */
public class Producer extends Thread {
    /**
     * blockingQueue:阻塞队列
     * random:用于产生随机整数
     */
    private BlockingQueue<String> blockingQueue;
    private Random random;

    public Producer(BlockingQueue blockingQueue) {
        this.blockingQueue = blockingQueue;
        this.random = new Random();
    }

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + " 生产者生产开始... " + new Date());
            /**
             * 循环随机产生3个字符串*/
            for (int i = 0; i < 3; i++) {
                TimeUnit.SECONDS.sleep(1 + random.nextInt(5));
                String info = "中国制造" + random.nextInt(100);
                /**往链式队列中添加元素,如果队列中的元素个数以已经达到队列容量,则put方法会阻塞
                 * 直到里面有元素被移除时,才能再次添加
                 * 链式队列添加元素与消费者是否已经消费无关,只要队列有容量,则可以一直添加*/
                blockingQueue.put(info);
                System.out.println(Thread.currentThread().getName() + " 生产了 " + info + " " + new Date());
            }
            System.out.println(Thread.currentThread().getName() + " 生产者生产结束,通知消费者结束... " + new Date());
            /**这里往队列中插入一个标识"exit",告诉它生产已经完毕,本任务即将结束*/
            blockingQueue.put("exit");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


/**
 * Created by Administrator on 2018/6/14 0014.
 * 消费者
 */
public class Consumer extends Thread {
    /**
     * blockingQueue:阻塞队列
     * countDownLatch:倒计数锁存器,用于将来告诉主线程消费者已经消费结束了
     * isExit:在收到生产者结束标识"exit"消息之前,消费者会一直循环阻塞的获取消息
     * isExittrue时,退出循环,消费任务结束
     * random:用于生产随机数
     */
    private BlockingQueue<String> blockingQueue;
    private CountDownLatch countDownLatch;
    public static boolean isExit = false;
    private Random random;

    /**
     * 初始化参数值
     *
     * @param blockingQueue
     * @param countDownLatch
     */
    public Consumer(BlockingQueue blockingQueue, CountDownLatch countDownLatch) {
        this.blockingQueue = blockingQueue;
        this.countDownLatch = countDownLatch;
        random = new Random();
    }

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + " 消费者开始消费... " + new Date());
            while (!isExit) {
                /**获取链式队列中头部元素,FIFO(先进先出)
                 * 队列中没有元素时,take会一直阻塞,获取到之后,队列原位置的元素就会被删除*/
                String info = blockingQueue.take();
                System.out.println(Thread.currentThread().getName() + " 消费了 " + info + " " + new Date());
                TimeUnit.SECONDS.sleep(1 + random.nextInt(5));
                /**如果收到"exit"消息,则退出循环*/
                if ("exit".equals(info)) {
                    isExit = true;
                }
            }
            System.out.println(Thread.currentThread().getName() + " 消费者结束消费... " + new Date());
            /**倒计数锁存器计数减一*/
            countDownLatch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/**
 * Created by Administrator on 2018/6/14 0014.
 * 测试类
 */
public class Test {
    public static void main(String[] args) {
        try {
            /**创建链式阻塞队列,并指定队列容量为10,注意当队列中元素个数达到10个时,使用put方法再次添加时则会阻塞
             * 普通的List即使设置了初始大小,超过容量时还是会自动改变容量;LinkedBlockingQueue一旦指定了就不会改变容量了
             * */
            BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>(10);
            /**因为会让主线程等待消费者线程,所以倒计数锁存器同步个数要与消费者个数一致*/
            CountDownLatch countDownLatch = new CountDownLatch(1);

            /**采用线程池方式
             * */
            ExecutorService executorService = Executors.newCachedThreadPool();

            /** 阻塞队列是线程安全的,无论多少生产和消费者都是可行的,注意点如下:
             * 一个生产者、一个消费者时:生产者的添加的元素都会被同一个消费者获取
             * 一个生产者、多个消费者时:生产者的添加的元素会被多个消费者获取,A获取了,则BC...其它所有的就没有了
             * 多个生产者、多个消费者时:所有生产者的添加的元素会放入同一个队列,而多个消费者则从队列中获取-
             * 队列中的元素被哪个消费者获取了之后,其余消费者就没有了
             */
            for (int i = 0; i < 2; i++) {
                if (i < 1) {
                    executorService.execute(new Producer(blockingQueue));
                } else {
                    executorService.execute(new Consumer(blockingQueue, countDownLatch));
                }
            }
            /**主线程开始阻塞,等待消费者消费完成*/
            countDownLatch.await();
            System.out.println("主线程开始关闭线程池...");
            executorService.shutdown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


------------------------结果输出-------------------------

pool-1-thread-2 消费者开始消费... Fri Jun 15 10:14:52 CST 2018
pool-1-thread-1 生产者生产开始... Fri Jun 15 10:14:52 CST 2018
pool-1-thread-1 生产了 中国制造5 Fri Jun 15 10:14:57 CST 2018
pool-1-thread-2 消费了 中国制造5 Fri Jun 15 10:14:57 CST 2018
pool-1-thread-1 生产了 中国制造79 Fri Jun 15 10:15:00 CST 2018
pool-1-thread-2 消费了 中国制造79 Fri Jun 15 10:15:01 CST 2018
pool-1-thread-1 生产了 中国制造36 Fri Jun 15 10:15:05 CST 2018
pool-1-thread-1 生产者生产结束,通知消费者结束... Fri Jun 15 10:15:05 CST 2018
pool-1-thread-2 消费了 中国制造36 Fri Jun 15 10:15:06 CST 2018
pool-1-thread-2 消费了 exit Fri Jun 15 10:15:08 CST 2018
pool-1-thread-2 消费者结束消费... Fri Jun 15 10:15:13 CST 2018
主线程开始关闭线程池...
Process finished with exit code 0





 

猜你喜欢

转载自blog.csdn.net/wangmx1993328/article/details/80695339