【小家java】BlockingQueue阻塞队列详解以及5大实现(ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue...)

版权声明: https://blog.csdn.net/f641385712/article/details/83691365

相关阅读

【小家java】java5新特性(简述十大新特性) 重要一跃
【小家java】java6新特性(简述十大新特性) 鸡肋升级
【小家java】java7新特性(简述八大新特性) 不温不火
【小家java】java8新特性(简述十大新特性) 饱受赞誉
【小家java】java9新特性(简述十大新特性) 褒贬不一
【小家java】java10新特性(简述十大新特性) 小步迭代
【小家java】java11新特性(简述八大新特性) 首个重磅LTS版本


【小家java】Java中的线程池,你真的用对了吗?(教你用正确的姿势使用线程池)
小家Java】一次Java线程池误用(newFixedThreadPool)引发的线上血案和总结
【小家java】BlockingQueue阻塞队列详解以及5大实现(ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue…)
【小家java】用 ThreadPoolExecutor/ThreadPoolTaskExecutor 线程池技术提高系统吞吐量(附带线程池参数详解和使用注意事项)


前言

在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景。

认识BlockingQueue

为什么说是阻塞(Blocking)的呢?是因为 BlockingQueue 支持当获取队列元素但是队列为空时,会阻塞等待队列中有元素再返回;也支持添加元素时,如果队列已满,那么等到队列可以放入新元素时再放入。

BlockingQueue 是一个接口,继承自 Queue,所以其实现类也可以作为 Queue 的实现来使用,而 Queue 又继承自 Collection 接口。

阻塞队列:顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示:
在这里插入图片描述
从上图我们可以很清楚看到,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;

常用的队列主要有以下两种策略:(当然通过不同的实现方式,还可以延伸出很多不同类型的队列,DelayQueue就是其中的一种)

  1. 先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。
  2. 后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。

多线程环境中,通过队列可以很容易实现数据共享,比如经典的**“生产者”和“消费者”模型中**,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。

但是生产者和消费者的处理速度,肯定是不完全匹配的。因此我们需要引入阻塞的概念:如果生产过剩,那就暂停一下等到消费者消费。反之亦然。然而在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节(比如我们使用Lock机制等),尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。

(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒)
当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。

作为BlockingQueue的使用者,我们再也不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。既然BlockingQueue如此神通广大,让我们一起来见识下它的常用方法:

BlockingQueue的核心方法:

BlockingQueue 对插入操作、移除操作、获取元素操作提供了四种不同的方法用于不同的场景中使用:
1、抛出异常;如果试图的操作无法立即执行,抛一个异常。
2、返回特殊值(null 或 true/false,取决于具体的操作);
3、阻塞等待此操作,直到这个操作成功;
4、阻塞等待此操作,直到成功或者超时指定时间。
详情如下:
在这里插入图片描述

BlockingQueue 的各个实现都遵循了这些规则,当然我们也不用死记这个表格,知道有这么回事,然后写代码的时候根据自己的需要去看方法的注释来选取合适的方法即可。

备注:peek 方法的语义是只读取不移除

放入数据:

  • offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程)
  • offer(E o, long timeout, TimeUnit unit),可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
  • put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被**阻断直到BlockingQueue里面有空间再继续.**

获取数据:

  • poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
  • poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则直到时间超时还没有数据可取,返回失败。
  • take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入**等待状态直到BlockingQueue有新的数据**被加入;
  • drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数), 通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁(一般用于批量操作)。

对于 BlockingQueue,我们的关注点应该在 put(e) 和 take() 这两个方法,因为这两个方法是带阻塞的。

BlockingQueue 是设计用来实现生产者-消费者队列的,当然,你也可以将它当做普通的 Collection 来用,前面说了,它实现了 java.util.Collection 接口。例如,我们可以用 remove(x) 来删除任意一个元素,但是,这类操作通常并不高效,所以尽量只在少数的场合使用,比如一条消息已经入队,但是需要做取消操作的时候。

BlockingQueue 的实现都是线程安全的,但是批量的集合操作如 addAll, containsAll, retainAll 和 removeAll 不一定是原子操作。如 addAll© 有可能在添加了一些元素后中途抛出异常,此时 BlockingQueue 中已经添加了部分元素,这个是允许的,取决于具体的实现

BlockingQueue常见5种实现

五大常用实现如下:
在这里插入图片描述

ArrayBlockingQueue

基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,因此并发效率上相对较低,这点尤其不同于LinkedBlockingQueue;

通过看源码分析,其实ArrayBlockingQueue完全是可以使用分离锁的。但是作者Doug Lea并没有这么去干,理由如下:

ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。

ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。

而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。

对于 ArrayBlockingQueue,我们可以在构造的时候指定以下三个参数:

public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
  1. 队列容量,其限制了队列中最多允许的元素个数;
  2. 指定独占锁是公平锁还是非公平锁。非公平锁的吞吐量比较高,公平锁可以保证每次都是等待最久的线程获取到锁;
  3. 可以指定用一个集合来初始化,将此集合中的元素在构造方法期间就先添加到队列中。

给个完整的生产者、消费者的例子:

public class ProduceConsumeDemo {

    public static void main(String[] args) {
        //生产者和消费者共用这一个队列,队列容量为10
        ArrayBlockingQueue<Cookie> arrayBlockingQueue = new ArrayBlockingQueue<>(10);

        //开启一个生产者
        for (int i = 0; i < 1; i++) {
            new Produce(arrayBlockingQueue).start();
        }

        //一个生产者,5个消费者
        for (int i = 0; i < 5; i++) {
            new Thread(new Consume(arrayBlockingQueue)).start();
        }

    }
}

class Produce extends Thread {
    private static int i = 0;
    private ArrayBlockingQueue<Cookie> arrayBlockingQueue;

    public Produce(ArrayBlockingQueue<Cookie> arrayBlockingQueue) {
        this.arrayBlockingQueue = arrayBlockingQueue;
    }

    public void run() {
        try {
            while (i < 1000) {
                arrayBlockingQueue.put(new Cookie("cookie" + i));
                if (++i % 100 == 0) {//每生产100个,休息10s
                    Thread.sleep(10000);
                }
            }
        } catch (InterruptedException e) {
            System.out.println("produce queue InterruptedException");
        }
    }
}

class Consume implements Runnable {
    private ArrayBlockingQueue<Cookie> arrayBlockingQueue;

    public Consume(ArrayBlockingQueue<Cookie> arrayBlockingQueue) {
        this.arrayBlockingQueue = arrayBlockingQueue;
    }

    public void run() {
        try {
            while (true) {
                Cookie poll = arrayBlockingQueue.poll(5, TimeUnit.SECONDS);//如果queue为null,那么5秒之后再去队列中取数据
                if (poll != null)
                    System.out.println(Thread.currentThread().getName() + "--consume --" + poll);

            }
        } catch (InterruptedException e) {
            System.out.println("consume queue InterruptedException");
        }
    }
}

class Cookie {
    private String number;

    public Cookie(String number) {
        this.number = number;
    }

    @Override
    public String toString() {
        return number + "";
    }
}
LinkedBlockingQueue

基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成)。当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回

只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

ArrayBlockingQueue和LinkedBlockingQueue是两个最普通也是最常用的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题,使用这两个类足以。

DelayQueue

DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

DelayQueue阻塞队列在我们系统开发中也常常会用到,例如:缓存系统的设计,缓存中的对象,超过了空闲时间,需要从缓存中移出;任务调度系统,能够准确的把握任务的执行时间。我们可能需要通过线程处理很多时间上要求很严格的数据,如果使用普通的线程,我们就需要遍历所有的对象,一个一个的检 查看数据是否过期等,首先这样在执行上的效率不会太高,其次就是这种设计的风格也大大的影响了数据的精度。一个需要12:00点执行的任务可能12:01 才执行,这样对数据要求很高的系统有更大的弊端。由此我们可以使用DelayQueue。

使用场景:
  DelayQueue使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。

使用:
为了具有调用行为,存放到DelayDeque的元素必须继承Delayed接口。Delayed接口使对象成为延迟对象,它使存放在DelayQueue类中的对象具有了激活日期。该接口强制执行下列两个方法。

  1. CompareTo(Delayed o):Delayed接口继承了Comparable接口,因此有了这个方法。
  2. getDelay(TimeUnit unit):这个方法返回到激活日期的剩余时间,时间单位由单位参数指定。

业务场景一:多考生考试
模拟一个考试的日子,考试时间为120分钟,30分钟后才可交卷,当时间到了,或者学生都交完卷了宣布考试结束。

实现思想:用DelayQueue存储考生(Student类),每一个考生都有自己的名字和完成试卷的时间,Teacher线程对DelayQueue进行监控,**收取完成试卷小于120分钟的学生的试卷。**当考试时间120分钟到时,先关闭Teacher线程,然后强制DelayQueue中还存在的考生交卷。每一个考生交卷都会进行一次

public class Exam {

    public static void main(String[] args) throws InterruptedException {
        int studentNumber = 20;
        CountDownLatch countDownLatch = new CountDownLatch(studentNumber + 1);
        DelayQueue<Student> students = new DelayQueue<>();
        Random random = new Random();
        for (int i = 0; i < studentNumber; i++) {
            students.put(new Student("student" + (i + 1), random.nextInt(120) + 30, countDownLatch));
        }

        //老师监考  准备一个老师即可  把学生们需要交给老师 以便监控
        Thread teacherThread = new Thread(new Teacher(students));

        //强制交卷 时间为120分钟  并且把老师线程穿进去收卷纸
        students.put(new EndExam(students, 120, countDownLatch, teacherThread));
        teacherThread.start();

        countDownLatch.await();
        System.out.println(" 考试时间到,全部交卷!");
    }

}

class Student implements Runnable, Delayed {

    private String name;
    private long workTime; //希望用时  有的学生小于120分钟  有的会大于120分钟 会被强制交卷
    private long submitTime; //交卷的时间
    private boolean isForce = false;
    private CountDownLatch countDownLatch;

    public Student() {
    }

    public Student(String name, long workTime, CountDownLatch countDownLatch) {
        this.name = name;
        this.workTime = workTime;
        this.submitTime = TimeUnit.NANOSECONDS.convert(workTime, TimeUnit.NANOSECONDS) + System.nanoTime();
        this.countDownLatch = countDownLatch;
    }

    @Override
    public int compareTo(Delayed o) {
        if (o == null || !(o instanceof Student))
            return 1;
        if (o == this)
            return 0;
        Student s = (Student) o;
        if (this.workTime > s.workTime) {
            return 1;
        } else if (this.workTime == s.workTime) {
            return 0;
        } else {
            return -1;
        }
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(submitTime - System.nanoTime(), TimeUnit.NANOSECONDS);
    }

    @Override
    public void run() {
        if (isForce) {
            System.out.println(name + " 交卷, 希望用时" + workTime + "分钟" + " ,实际用时 120分钟");
        } else {
            System.out.println(name + " 交卷, 希望用时" + workTime + "分钟" + " ,实际用时 " + workTime + " 分钟");
        }
        countDownLatch.countDown(); //交卷一个 减一个人
    }

    public boolean isForce() {
        return isForce;
    }

    public void setForce(boolean isForce) {
        this.isForce = isForce;
    }

}

class EndExam extends Student {

    private DelayQueue<Student> students;
    private CountDownLatch countDownLatch;
    private Thread teacherThread;

    public EndExam(DelayQueue<Student> students, long workTime, CountDownLatch countDownLatch, Thread teacherThread) {
        super("强制收卷", workTime, countDownLatch);
        this.students = students;
        this.countDownLatch = countDownLatch;
        this.teacherThread = teacherThread;
    }


    @Override
    public void run() {
        teacherThread.interrupt(); //打断线程 强制交卷  不要让学生自己take了 也可采用一个全局变量标记
        Student tmpStudent;

        //遍历所有还未执行的学生们 把他们拿出来 手动调用他们的run方法交卷
        for (Iterator<Student> iterator2 = students.iterator(); iterator2.hasNext(); ) {
            tmpStudent = iterator2.next();
            tmpStudent.setForce(true);
            tmpStudent.run();
        }
        countDownLatch.countDown(); //最后注意 把自己强制交卷的线程 也要一下
    }

}

class Teacher implements Runnable {

    // 老师需要知道  自己监控哪些学生 宣布考试开始
    private DelayQueue<Student> students;

    public Teacher(DelayQueue<Student> students) {
        this.students = students;
    }

    @Override
    public void run() {
        try {
            System.out.println(" test start");
            //宣布考试后 才能让学生开始run
            // 此处需要注意 take是阻塞的 只要时间到了  才会take出来 才会执行run方法
            // 中途有可能线程会被interrupted(比如强制交卷的情况下 就不能再让take了 需要执行强制交卷的线程任务)
            while (!Thread.interrupted()) {
                students.take().run();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
输出:
 test start
student9 交卷, 希望用时35分钟 ,实际用时 35 分钟
student5 交卷, 希望用时45分钟 ,实际用时 45 分钟
student6 交卷, 希望用时45分钟 ,实际用时 45 分钟
student20 交卷, 希望用时52分钟 ,实际用时 52 分钟
student11 交卷, 希望用时62分钟 ,实际用时 62 分钟
student2 交卷, 希望用时79分钟 ,实际用时 79 分钟
student15 交卷, 希望用时81分钟 ,实际用时 81 分钟
student8 交卷, 希望用时83分钟 ,实际用时 83 分钟
student10 交卷, 希望用时83分钟 ,实际用时 83 分钟
student1 交卷, 希望用时83分钟 ,实际用时 83 分钟
student18 交卷, 希望用时94分钟 ,实际用时 94 分钟
student3 交卷, 希望用时94分钟 ,实际用时 94 分钟
student16 交卷, 希望用时98分钟 ,实际用时 98 分钟
student7 交卷, 希望用时114分钟 ,实际用时 114 分钟
student12 交卷, 希望用时118分钟 ,实际用时 118 分钟
student19 交卷, 希望用时122分钟 ,实际用时 120分钟
student17 交卷, 希望用时125分钟 ,实际用时 120分钟
student14 交卷, 希望用时134分钟 ,实际用时 120分钟
student4 交卷, 希望用时148分钟 ,实际用时 120分钟
student13 交卷, 希望用时143分钟 ,实际用时 120分钟
 考试时间到,全部交卷!

业务场景二:具有过期时间的缓存
向缓存添加内容时,给每一个key设定过期时间,系统自动将超过过期时间的key清除。
这个场景中几个点需要注意:

  1. 当向缓存中添加key-value对时,如果这个key在缓存中存在并且还没有过期,需要用这个key对应的新过期时间
  2. 为了能够让DelayQueue将其已保存的key删除,需要重写实现Delayed接口添加到DelayQueue的DelayedItem的hashCode函数和equals函数
  3. 当缓存关闭,监控程序也应关闭,因而监控线程应当用守护线程
/**
 * 利用延迟队列,来书写一个具有过期key效果的简单缓存  缓存使用ConcurrentHashMap实现
 *
 * @author [email protected]
 * @description //
 * @date 2018/11/3 21:25
 */
public class Cache<K, V> {

    //模拟装载缓存数据
    public ConcurrentHashMap<K, V> map = new ConcurrentHashMap<>();
    //缓存即将要过期的key们
    public DelayQueue<DelayedItem<K>> queue = new DelayQueue<>();

    public static void main(String[] args) throws InterruptedException {
        Random random = new Random();
        int cacheNumber = 10;
        int liveTime = 0;
        Cache<String, Integer> cache = new Cache<>();

        for (int i = 0; i < cacheNumber; i++) {
            liveTime = random.nextInt(3000);
            System.out.println(i + "  " + liveTime);
            cache.put(i + "", i, random.nextInt(liveTime));

            if (random.nextInt(cacheNumber) > 7) {
                liveTime = random.nextInt(3000);
                System.out.println(i + "  " + liveTime);
                cache.put(i + "", i, random.nextInt(liveTime));
            }
        }

        Thread.sleep(3000);
        System.out.println("--------------");
    }


    /**
     * 向缓存里面添加元素  可以指定key的存活时间
     *
     * @param k
     * @param v
     * @param liveTime
     */
    public void put(K k, V v, long liveTime) {
        V v2 = map.put(k, v);
        DelayedItem<K> tmpItem = new DelayedItem<>(k, liveTime);

        //把旧的移除掉 若存在旧的话
        if (v2 != null) {
            queue.remove(tmpItem);
        }
        queue.put(tmpItem);
    }

    //创建缓存对象的时候 开启一个守护线程 一直不停的去检查 阻塞队列里面是否有元素需要过期了移出来
    public Cache() {
        Thread t = new Thread(() -> {
            while (true) {
                DelayedItem<K> delayedItem = queue.poll(); //阻塞
                if (delayedItem != null) {
                    map.remove(delayedItem.getT());
                    System.out.println(System.nanoTime() + " remove " + delayedItem.getT() + " from cache");
                }
                try {
                    Thread.sleep(300);
                } catch (Exception e) {
                }
            }
        });
        t.setDaemon(true); //一定需要是守护线程
        t.start();
    }

}


class DelayedItem<T> implements Delayed {

    private T t;
    private long liveTime;
    private long removeTime;

    public DelayedItem(T t, long liveTime) {
        this.setT(t);
        this.liveTime = liveTime;
        this.removeTime = TimeUnit.NANOSECONDS.convert(liveTime, TimeUnit.NANOSECONDS) + System.nanoTime();
    }

    @Override
    public int compareTo(Delayed o) {
        if (o == null) return 1;
        if (o == this) return 0;
        if (o instanceof DelayedItem) {
            DelayedItem<T> tmpDelayedItem = (DelayedItem<T>) o;
            if (liveTime > tmpDelayedItem.liveTime) {
                return 1;
            } else if (liveTime == tmpDelayedItem.liveTime) {
                return 0;
            } else {
                return -1;
            }
        }
        //按照getDelay来比较即可  因为有可能传进俩的对象  并不是DelayedItem对象,而是别的Delayed对象
        long diff = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
        return diff > 0 ? 1 : diff == 0 ? 0 : -1;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(removeTime - System.nanoTime(), unit);
    }

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

    //必须重写啊  因为需要根据key移除 底层依赖hashCode和equals
    @Override
    public int hashCode() {
        return t.hashCode();
    }

    //必须重写啊  因为需要根据key移除 底层依赖hashCode和equals
    @Override
    public boolean equals(Object object) {
        if (object instanceof DelayedItem) {
            return object.hashCode() == hashCode() ? true : false;
        }
        return false;
    }

}
输出:
0  2039
1  782
1  2354
2  1052
3  1078
4  2816
5  200
6  112
7  563
8  1383
8  900
9  2760
16353215371312 remove 6 from cache
16353515890864 remove 7 from cache
16353820262116 remove 5 from cache
16354120636676 remove 3 from cache
16354421318587 remove 8 from cache
16354722121324 remove 2 from cache
16355023081512 remove 1 from cache
16355323876697 remove 0 from cache
16355623834030 remove 4 from cache
16355924335835 remove 9 from cache
--------------

PriorityBlockingQueue

基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。

因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。

SynchronousQueue

一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。

相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。

声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:

public SynchronousQueue() {
    this(false);
}
 
public SynchronousQueue(boolean fair) {
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
  1. 如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
  2. 如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理

你不能在 SynchronousQueue 中使用 peek 方法(在这里这个方法直接返回 null),peek 方法的语义是只读取不移除,显然,这个方法的语义是不符合 SynchronousQueue 的特征的。SynchronousQueue 也不能被迭代,因为根本就没有元素可以拿来迭代的。虽然 SynchronousQueue 间接地实现了 Collection 接口,但是如果你将其当做 Collection 来用的话,那么集合是空的。

SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合传递性场景。SynchronousQueue的吞吐量高于ArrayBlockingQueue和LinkedBlockingQueue。

/**
 * 使用SynchronousQueue演示生产者、消费者模型
 *
 * @author [email protected]
 * @description //
 * @date 2018/11/4 9:46
 */
public class Main {

    public static void main(String[] args) {
        SynchronousQueue<Integer> queue = new SynchronousQueue<>();
        new Customer(queue).start();
        new Product(queue).start();
    }

    static class Product extends Thread {
        SynchronousQueue<Integer> queue;

        public Product(SynchronousQueue<Integer> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            while (true) {
                int rand = new Random().nextInt(1000);
                System.out.println("生产了一个产品:" + rand);
                System.out.println("等待三秒后运送出去...");
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                queue.offer(rand);
                System.out.println("产品生成完成:" + rand);
            }
        }
    }

    static class Customer extends Thread {
        SynchronousQueue<Integer> queue;

        public Customer(SynchronousQueue<Integer> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    System.out.println("消费了一个产品:" + queue.take());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("------------------------------------------");
            }
        }
    }
}
输出:
生产了一个产品:214
等待三秒后运送出去...
产品生成完成:214
消费了一个产品:214
------------------------------------------
生产了一个产品:713
等待三秒后运送出去...
消费了一个产品:713
------------------------------------------
产品生成完成:713
生产了一个产品:38
等待三秒后运送出去...
产品生成完成:38
生产了一个产品:93
等待三秒后运送出去...
消费了一个产品:38
------------------------------------------

从结果中可以看出如果已经生产但是还未消费的,那么会阻塞在生产一直等到消费才能生成下一个。

这是单线程版本,若是多线程版本,结果相信大家也能猜到。请大家自己尝试书写

总结


ArrayBlockingQueue 底层是数组,有界队列,如果我们要使用生产者-消费者模式,这是非常好的选择。

LinkedBlockingQueue 底层是链表,可以当做无界和有界队列来使用,所以大家不要以为它就是无界队列。

DelayQueue是一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的Delayed 元素。

SynchronousQueue 本身不带有空间来存储任何元素,使用上可以选择公平模式和非公平模式。

PriorityBlockingQueue 是无界队列,基于数组,数据结构为二叉堆,数组第一个也是树的根节点总是最小值。


BlockingQueue不光实现了一个完整队列所具有的基本功能,同时在多线程环境下,他还自动管理了多线程间的自动等待于唤醒功能,从而使得程序员可以忽略这些细节,关注更高级的功能。

另外提一点:非阻塞队列

在并发编程中,有时候需要使用线程安全的队列。如果要实现一个线程安全的队列有两种方式:一种是使用阻塞算法,另一种是使用非阻塞算法。
非阻塞队列:
ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。
LinkedList继承AbstractSequentialList,实现了List接口和Deque接口,元素可以为null。非线程安全。LinkedList 内部使用的数据结构是一个双向链表。如果要经常执行插入和删除操作,可以考虑使用LinkedList。使用迭代器遍历LinkedList。

**在并发编程中,一般推荐使用阻塞队列,这样实现可以尽量地避免程序出现意外的错误。**阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。还有其他类似的场景,只要符合生产者-消费者模型的都可以使用阻塞队列。

使用非阻塞队列,虽然能即时返回结果(消费结果),但必须自行编码解决返回为空的情况处理(以及消费重试等问题)。

猜你喜欢

转载自blog.csdn.net/f641385712/article/details/83691365