• JavaEE-多线程-阻塞队列


    什么是阻塞队列

    BlockingQueue阻塞队列,排队拥堵,首先它是一个队列,而一个阻塞队列在数据结构中所起到的作用大致如下午所示:

    线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素
    当阻塞队列是时,从队列中获取元素的操作将会被阻塞
    当阻塞队列是时,从队列中添加元素的操作将会被阻塞
    也就是说 试图从空的阻塞队列获取元素的线程将会被阻塞直到其它线程往空的队列插入新的元素
    同理,试图往已经满的阻塞队列添加新元素的线程,直到其它线程往满的队列中移除一个或多个元素,或者完全清空队列后,使队列重新变得空闲起来,并后续新增

    为什么要有阻塞队列BlockingQueue

    多线程领域:所谓的阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动唤醒
    使用阻塞队列的好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都帮你一手包办
    在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己取控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
    BlockingQueue阻塞队列是属于一个接口,底下有七个实现类

    • ArrayBlockQueue:由数组结构组成的有界阻塞队列
    • LinkedBlockingQueue:由链表结构组成的有界(但是默认大小 Integer.MAX_VALUE)的阻塞队列
      • 有界,但是界限非常大相当于无界,可以当成无界
    • PriorityBlockQueue:支持优先级排序无界阻塞队列
    • DelayQueue:使用优先级队列实现的延迟无界阻塞队列
    • SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列
      • 生产一个,消费一个不存储元素不消费不生产
    • LinkedTransferQueue:由链表结构组成的无界阻塞队列
    • LinkedBlockingDeque:由链表结构组成的双向阻塞队列

    阻塞队列引入

    阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法:
    1)支持阻塞的插入方法:意思是当队列时,队列会阻塞插入元素的线程,直到队列不满
    2)支持阻塞的移除方法:意思是在队列为时,获取元素的线程会等待队列变为非空

    阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程阻塞队列就是生产者用来存放元素消费者用来获取元素容器

    在阻塞队列不可用时,这两个附加操作提供了4种处理方式

    方法/处理方式抛出异常返回特殊值一直阻塞超时退出
    插入方法add(e)offer(e)put(e)offer(e,time,unit)
    移除方法remove()poll()take()poll(time,unit)
    检查方法element()peek()不可用🙅‍不可用🙅‍

    抛出异常:是指当阻塞队列时候,再往队列里插入元素,会抛出 IllegalStateException(“Queue full”)** 异常**。当队列为时,从队列里获取元素时会抛出 NoSuchElementException** 异常** 。

    返回特殊值插入方法会返回是否成功,成功则返回 true移除方法,则是从队列里拿出一个元素,如果没有则返回 null

    一直阻塞:当阻塞队列时,如果生产者线程往队列里 put 元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里 take 元素,队列也会阻塞消费者线程,直到队列可用。

    超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出

    如果是无界阻塞队列,队列不可能会出现满的情况,所以使用put或offer方法永远不会被阻塞,而且使用offer方法时,该方法永远返回true

    阻塞队列

    JDK7 提供了 7 个阻塞队列。分别是

    ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。

    LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。

    PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

    DelayQueue:一个使用优先级队列实现的无界阻塞队列。

    SynchronousQueue:一个不存储元素的阻塞队列。

    LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

    LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列

    ArrayBlockingQueue

    ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序
    image.png

    • items:一个Object的数组
    • tackIndex:出队列的下标
    • putIndex:入队列的下标
    • count:队列中元素的数量

    ArrayBlockingQueue 是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列

    ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);

    查看ArrayBlockingQueue 的构造函数
    image.png
    设置ReentrantLock的锁模式为公平锁
    image.png

    LinkedBlockingQueue

    LinkedBlockingQueue 是一个用链表实现的有界阻塞队列。此队列的默认最大长度为 Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序
    image.png
    ArrayBlockingQueue 和LinkedBlockingQueue区别
    1:底层实现不同
    ArrayBlockingQueue 底层使用数组来维护队列,
    LinkedBlockingQueue 底层使用链表来维护队列,在添加和删除队列中的元素的时候,会创建和销毁节点对象,在高并发和大量数据的时候,GC压力很大
    2:锁的方式不同
    ArrayBlockingQueue 获取数据和添加数据都是使用同一个锁对象,这样添加和获取就不是一个并发的过程,不过,在ArrayBlockingQueue 中使用Condition的等待/通知机制,这样使得ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜
    LinkedBlockingQueue 获取数据和添加数据使用不同的锁对象

    PriorityBlockingQueue

    PriorityBlockingQueue是一个支持优先级无界阻塞队列。默认情况下元素采取自然顺序升序排列。继承Comparable类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
    例子 学生实体类存入PriorityBlockingQueue 队列按照年龄升序排序

    package blockingqueue;
    
    import java.util.Formattable;
    import java.util.concurrent.PriorityBlockingQueue;
    
    public class PriorityBlockingQueueTest {
        public static class Student implements Comparable<Student> {
            private String name;
            private int age;
            public Student(String name,int age){
                this.name = name;
                this.age = age;
            }
            public String getName(){
                return name;
            }
    
            public void setName(String name) {
                this.name = name;
            }
    
            public int getAge() {
                return age;
            }
    
            public void setAge(int age) {
                this.age = age;
            }
    
            @Override
            public int compareTo(Student o) {
                return this.age > o.getAge() ? 1 : this.age <o.getAge() ? -1 : 0;
            }
    
            @Override
            public String toString() {
                return "Student{" +
                        "name='" + name + '\'' +
                        ", age=" + age +
                        '}';
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            PriorityBlockingQueue<Student> queue = new PriorityBlockingQueue<Student>();
            queue.put(new Student("小A",18));
            queue.put(new Student("小B",17));
            queue.put(new Student("小C",19));
            queue.put(new Student("小D",20));
    
            while (true){
                System.out.println(queue.take().toString());
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    image.png

    DelayQueue

    DelayQueue是一个支持延时获取元素无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素

    DelayQueue非常有用,可以将DelayQueue运用在以下应用场景。

    缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。

    定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的

    实现DelayQueue的三个步骤

    第一步:继承Delayed接口

    第二步:实现getDelay(TimeUnit unit),该方法返回当前元素还需要延时多长时间,单位是纳秒

    第三步:实现compareTo方法来指定元素的顺序

    例子:使用Task给定延迟时间继承Delayed,并**重写getDelay()方法,然后重写compareTo()**方法来规定先到期的Task对象先出队列

    package blockingqueue;
    
    import java.util.concurrent.DelayQueue;
    import java.util.concurrent.Delayed;
    import java.util.concurrent.TimeUnit;
    
    public class DelayQueueTest {
        public static class Task implements Delayed {
            private long outTime;//延迟时间
            private long delayTime;//到期时间
    
            public Task(long outTime){
                this.outTime = outTime;
                //使用的时间刻度是纳秒级别
                delayTime = System.nanoTime() + TimeUnit.NANOSECONDS.convert(outTime,TimeUnit.SECONDS);
            }
    
            /**
             * 获取剩余的到期时间 当返回负数的时候代表过期
             * @param unit the time unit
             * @return
             */
            public long getDelay(TimeUnit unit){
                return unit.convert(this.delayTime -
                        System.nanoTime(),TimeUnit.NANOSECONDS);
            }
    
            /**
             * 对延迟时间排序
             * @param o the object to be compared.
             * @return
             */
            public int compareTo(Delayed o){
                if (o == this) return 0;
                if (o instanceof Task){
                    Task task = (Task)o;
                    return Long.compare(this.outTime, task.outTime);
                }
                long d = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
                return d > 0 ? 1 : d < 0 ? -1 : 0;
            }
            public void print(){
                System.out.println("task_"+outTime+"到期");
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            final DelayQueue<Task> queue = new DelayQueue<Task>();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0;i<11;i++){
                        Task task = new Task(i);
                        queue.put(task);
                    }
                }
            },"Produce-Thread").start();
            Thread.sleep(1000);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (;;){
                        Task task = null;
                        try {
                            task = queue.take();
                            task.print();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            },"Consume-Thread").start();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    SychronousQueue

    SychronousQueue是一个不存储元素的阻塞队列.每一个put操作必须等待一个take操作,否则不能继续添加元素.
    它支持公平访问队列. 默认情况下线程采用非公平性策略访问队列.使用以下构造方法可以创建公平性访问的SynchronousQueue,如果设置为true,则等待的线程会采用先进先出的顺序访问队列

    public SynchronousQueue(boolean fair){
        transferer = fair ? new Transfer Queue<E>() : new TransferStack<E>();
    }
    
    • 1
    • 2
    • 3

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

    LinkedTransferQueue

    LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfertransfer方法

    (1)transfer方法
    如果当前有消费者正在等待接收元素(消费者使用take()方法带时间限制poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回

    (2)tryTransfer方法
    tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收方法立即返回,而transfer方法是必须等到消费者消费了才返回。
    对于带有时间限制的tryTransfer(E e,long timeout,TimeUnit unit)方法,试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true

    LinkedBlockingDeque

    LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的是可以从队列的两端插入移出元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque多了addFirstaddLast、offerFirst、offerLast、peekFirst和peekLast等方法,以First单词结尾的方法,表示插入获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法,表示插入、获取或移除双端队列的最后一个元素。另外,插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,不知道是不是JDK的bug,使用时还是用带有First和Last后缀的方法更清楚。在初始化LinkedBlockingDeque时可以设置容量防止其过度膨胀。另外,双向阻塞队列可以运用在**“工作窃取”**模式中。

    生产者与消费者模型

    生产者消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段共用同一存储空间,生产者向空间里生产数据,而消费者取走数据
    阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
    image.png

    实现

    通过 “循环队列” 的方式来实现.
    使用 synchronized 进行加锁控制.
    put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一
    定队列就不满了, 因为同时可能是唤醒了多个线程).
    take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)

    package thread;
    
    // 自己模拟实现一个阻塞队列.
    // 基于数组的方式来实现队列.
    // 提供两个核心方法:
    // 1. put 入队列
    // 2. take 出队列
    class MyBlockingQueue {
        // 假定最大是 1000 个元素. 当然也可以设定成可配置的.
        private int[] items = new int[1000];
        // 队首的位置
        private int head = 0;
        // 队尾的位置
        private int tail = 0;
        // 队列的元素个数
        volatile private int size = 0;
    
        // 入队列
        public void put(int value) throws InterruptedException {
            synchronized (this) {
                while (size == items.length) {
                    // 队列已满, 无法插入
                    this.wait();
                }
                items[tail] = value;
                tail++;
                // 汤老湿个人还是更建议写这个版本.
                if (tail == items.length) {
                    // 注意 如果 tail 达到数组末尾, 就需要从头开始~
                    tail = 0;
                }
                // 下面这个写法绝对不是错!! 也是正确的写法. 大家去掌握当然也没毛病
                // tail = tail % items.length;
                size++;
                // 即使没人在等待, 多调用几次 notify 也没啥副作用~~
                this.notify();
            }
        }
    
        // 出队列
        public Integer take() throws InterruptedException {
            int ret = 0;
            synchronized (this) {
                while (size == 0) {
                    // 队列为空, 就等待
                    this.wait();
                }
                ret = items[head];
                head++;
                if (head == items.length) {
                    head = 0;
                }
                size--;
                this.notify();
            }
            return ret;
        }
    }
    
    public class Demo21 {
        public static void main(String[] args) throws InterruptedException {
            MyBlockingQueue queue = new MyBlockingQueue();
            Thread customer = new Thread(() -> {
                while (true) {
                    int value = 0;
                    try {
                        value = queue.take();
                        System.out.println("消费: " + value);
    
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            customer.start();
    
            Thread producer = new Thread(() -> {
                int value = 0;
                while (true) {
                    try {
                        queue.put(value);
                        System.out.println("生产: " + value);
                        value++;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            producer.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92

    希望能帮到你

  • 相关阅读:
    优先级反转那些事儿
    高压放大器电源有什么作用和用途
    CSS 3之背景属性
    【设计模式】27.结构型模式-适配器(Adapter)
    latex如何对.pdf格式的图片实现裁剪
    JWT 弊端解决思路(主动过期\权限更新)
    postgresql主从复制、主从切换
    Flink核心API之Table API和SQL
    查看磁盘信息命令和查看目录以及文件占用空间大小命令
    探索GitHub上的两个革命性开源项目
  • 原文地址:https://blog.csdn.net/qq_63511424/article/details/126168713