分享

阻塞队列

 贪挽懒月 2022-06-20 发布于广东

阻塞

队列

BLOKING

QUEUE

什么是阻塞队列

阻塞队列,英文名BlockingQueue,顾名思义,首先它是一个队列。阻塞就是说在多线程环境下,线程在某些情况下会被挂起,这就是阻塞,一旦满足条件,又会被唤醒。那么阻塞队列的阻塞体现在何处?当一个线程从阻塞队列中取元素时,如果队列为空了,那么取元素的操作就会被阻塞,直到有其他线程往队列中添加了元素;当一个线程往阻塞队列中添加元素时,如果队列满了,那么添加元素的操作也会被阻塞,直到有其他线程从队列中取走了元素。

为什么要用阻塞队列

有了阻塞队列,我们不需要关心何时阻塞线程,何时唤醒线程。因为这些操作阻塞队列都帮我们做了。队列为空那么取元素的线程会自动被阻塞,队列已满那么添加元素的线程会自动阻塞。

阻塞队列架构梳理

之前我们只知道 Collection 下面有 set 和 list,其实 queue也是继承了 Collectionqueue的子类就是 BlockingQueue,不过它还是接口,它总共有九个实现类,下面挑三个最重要的说一说。

  • ArrayBlockingQueue:按照 ArrayList 来理解,由数组结构组成的有界阻塞队列。何为有界?其实就是队列的容量。看一下它的构造方法:

1public ArrayBlockingQueue(int capacity) {
2        this(capacity, false);
3}

容量调用构造方法时必传的参数,默认是非公平的,也就是说,它的方法都用Lock上了锁,我们知道lock可以通过true或者false来指定使用公平锁还是非公平锁。所以这里传的false或者true其实是给lock用的。

  • LinkedBlockingQueue:按照 LinkedList 来理解,由链表结构组成的有界阻塞队列。其实这里有个坑爹的地方,先来看一下它的构造方法:

1public LinkedBlockingQueue() {
2        this(Integer.MAX_VALUE);
3}

它有个无参构造,我们可以不传容量,默认是 ** int类型的最大值**,这不是坑爹嘛,int类型最大值是21亿多,这不就相当于无界嘛。当然,它也有带参构造,可以指定容量,但是不能指定使用公平还是非公平锁,默认使用的是非公平锁。

  • SynchronousQueue:这个队列比较特殊,只能存储一个元素。里面有一个元素的时候,添加的线程就会被阻塞。

阻塞队列核心方法

阻塞队列的核心方法有四组,如下表:

方法类型抛出异常特殊值阻塞超时
插入add(e)offer(e)put(e)offer(e,time,unit)
移除removepoll()take()poll(time,unit)
检查element()peek()
抛出异常就是说队列中没有元素还去取或者元素满了还添加,那么就抛出异常。其他几组也是按照这思路理解,都是顾名思义的,此处不再啰嗦。
阻塞队列用在哪儿

1、用在哪?
阻塞队列有哪些应用呢?常见的有以下三个:

  • 生产消费模式

  • 线程池

  • 消息中间件

本文将讲解生产消费模式中如何使用阻塞队列。

2、生产消费模式:

  • 多线程编程的口诀
    线程 操纵 资源类,判断 干活 通知;防止虚假唤醒(判断一定要用while,不能用if)。

  • 基础版生产消费模式
    资源类:

 1class Resource{
2    private  Integer num = 0;
3    private Lock lock = new ReentrantLock();
4    private Condition condition = lock.newCondition();
5    public void produce()// 生产的方法
6        lock.lock();
7        try {
8            // 1.判断
9            while (num != 0){
10                // 等待,不能生产
11                condition.await();
12            }
13            // 2.干活
14            num ++;
15            System.out.println(Thread.currentThread().getName() + "\t" + num);
16            // 3.通知
17            condition.signalAll();
18        }catch (Exception e){
19            e.printStackTrace();
20        }finally {
21            lock.unlock();
22        }
23    }
24
25    public void consum()// 消费的方法
26        lock.lock();
27        try {
28            // 1.判断
29            while (num == 0){
30                // 等待,不能消费
31                condition.await();
32            }
33            // 2.干活
34            num --;
35            System.out.println(Thread.currentThread().getName() + "\t" + num);
36            // 3.通知
37            condition.signalAll();
38        }catch (Exception e){
39            e.printStackTrace();
40        }finally {
41            lock.unlock();
42        }
43    }
44}

线程操纵资源类:

 1public static void main(String[] args){
2        Resource resource = new Resource();
3        new Thread(() -> {
4            for (int i=1; i<=5; i++){
5                resource.produce();
6            }
7        },"A").start();
8
9        new Thread(() -> {
10            for (int i=1; i<=5; i++){
11                resource.consum();
12            }
13        },"B").start();
14}

这就是基础版的生产消费模式。准确的说应该是2.0版本,最开始学的是使用synchronized实现的。那么,synchronized和lock到底有什么区别呢?

  • synchronized 和 lock 的区别:

原始构成使用方法等待是否可中断是否公平唤醒
synchronized是关键字,属于JVM层面,底层通过monitor对象来完成不需要用户手动释放锁,锁住的代码执行完后系统会自动让线程释放对锁的占用不可中断,除非抛异常或者正常运行完成非公平锁只能随机唤醒一个或者唤醒所以线程
ReentrantLock是JUC中的一个类,是API层面的锁需要手动释放锁,若没释放,则可能导致死锁可中断默认非公平,可设置为公平锁可以精确唤醒

这里来说一说ReentrantLock的精确唤醒。现有题目如下:

1有A、B、C三个线程,A打印5次,B打印10次,C打印15次,然后又是A打印5次,B打印10次,C打印15次……循环10轮。

这就是经典的线程按序交替问题。看看如何使用 ReentrantLock 来解决。

 1/** 资源类 */
2class Resource{
3    private   Integer flag = 1// 1: A执行,2:B执行,3:C执行
4    private  Lock lock = new ReentrantLock();
5    public Condition condition1 = lock.newCondition();
6    public Condition condition2 = lock.newCondition();
7    public  Condition condition3 = lock.newCondition();
8
9    public void print(Condition waitCondition, Condition signalCondition, 
10                      Integer num, Integer nowFlag, Integer changedFlag
)
{
11        lock.lock();
12        try { // 1. 判断
13            while (this.flag != nowFlag){
14                waitCondition.await();
15            }
16            // 2. 干活
17            for (int i=1; i<=num; i++){
18                System.out.println(Thread.currentThread().getName() + "\t" + i);
19            }
20            // 3. 通知
21            this.flag = changedFlag;
22            signalCondition.signal();
23        }catch (Exception e){
24        }finally {
25            lock.unlock();
26        }
27    }
28}

这是资源类,一把锁,因为有三个线程,需要精确唤醒,就需要三个condition;flag是一个标识,用来判断是哪个线程进行执行;print方法有5个参数,第一个是等待的线程的condition,第二个是需要唤醒的线程的condition,第三个是打印的次数,第四个是当前的flag,第五个是需要修改的flag值。看看线程如何操作这个资源类:

 1public static void main(String[] args){
2        Resource resource = new Resource();
3        new Thread(() -> {
4            for (int i=1; i<=10; i++){
5                resource.print(resource.condition1, resource.condition2, 512);
6                System.out.println("========================================================");
7               }
8           },"A").start();
9
10        new Thread(() -> {
11            for (int i=1; i<=10; i++){
12                resource.print(resource.condition2, resource.condition3, 1023);
13                System.out.println("========================================================");
14            }
15        },"B").start();
16        new Thread(() -> {
17            for (int i=1; i<=10; i++){
18                resource.print(resource.condition3, resource.condition1, 1531);
19                System.out.println("========================================================");
20            }
21        },"C").start();
22}

首先创建A线程,看看A线程调用print方法传入参数后是什么情况:

 1lock.lock();
2try { // 1. 判断
3    while (this.flag != 1){
4        condition1.await();
5    }
6    // 2. 干活
7    for (int i=1; i<=5; i++){
8        System.out.println(Thread.currentThread().getName() + "\t" + i);
9    }
10    // 3. 通知
11    this.flag = 2;
12    condition2.signal();
13}catch (Exception e){ 
14}finally {
15    lock.unlock();
16}

线程A调用时,首先判断flag是不是1,如果不是,那么线程A就等待,否则就干活,打印5次。干完活要让线程B执行,所以将flag修改为2,然后将线程B唤醒。线程B调用时,自己干完活就唤醒C,线程C干完活就唤醒A……所以执行结果就是:

线程按序交替
  • 阻塞队列版生产消费模式:
    资源类:

 1class Resource {
2    private volatile boolean flag = true// 标识
3    private AtomicInteger atomicInteger = new AtomicInteger();
4    BlockingQueue<Integer> blockingQueue = null;
5
6    public Resource(BlockingQueue<Integer> blockingQueue) {
7        this.blockingQueue = blockingQueue;
8    }
9
10    // 生产的方法
11    public void produce() throws Exception {
12        Integer data = null;
13        boolean result;
14        while (flag) {
15            data = atomicInteger.incrementAndGet();
16            result = blockingQueue.offer(data2L, TimeUnit.SECONDS);
17            if (result)
18                System.out.println(Thread.currentThread().getName() +"成功生产"data +" 号蛋糕!");
19            else
20                System.out.println(Thread.currentThread().getName() + "生产蛋糕失败!");
21            TimeUnit.SECONDS.sleep(1);
22        }
23        System.out.println(Thread.currentThread().getName() + "停止生产!");
24    }
25
26    // 消费的方法
27    public void consume() throws Exception {
28        System.out.println();
29        Integer data = null;
30       while (flag){
31           data = blockingQueue.poll(2L, TimeUnit.SECONDS);
32           if (data == null){
33               flag = false;
34               System.out.println(Thread.currentThread().getName()+"超过2秒没取到,停止消费!");
35               return;
36           }
37           System.out.println(Thread.currentThread().getName() +"成功消费" +data" 号蛋糕!");
38       }
39    }
40
41    // 停止的方法
42    public void stop() throws Exception {
43        this.flag = false;
44    }
45}

首先,当flag为true时,进行生产,就是将 atomicInteger 进行自增,放到阻塞队列中;放完一个就休息1秒钟。然后是消费的方法,也是当flag为true就进行消费,消费就是从阻塞队列中取出元素,如果取到的是 null,说明队列中没有元素了,就将flaf设为false,退出循环,停止消费。最后的停止的方法,就是将flag设为false,这样生产和消费都会停止。
操纵资源类:

 1public static void main(String[] args) throws Exception{
2        Resource resource = new Resource(new ArrayBlockingQueue<>(6));
3        new Thread(() -> {
4            System.out.println("生产线程启动!");
5            try {
6                resource.produce();
7            } catch (Exception e) {
8                e.printStackTrace();
9            }
10
11        }, "生产线程 ").start();
12
13        new Thread(() -> {
14            System.out.println("消费线程启动!");
15            try {
16                resource.consume();
17            } catch (Exception e) {
18                e.printStackTrace();
19            }
20        }, "消费线程 ").start();
21        // 5秒钟后停止
22        TimeUnit.SECONDS.sleep(5);
23        System.out.println();
24        System.out.println("5秒钟后停止生产!");
25        resource.stop();
26}

这里是创建了两个线程,一个调用生产方法,一个调用消费方法;然后让主线程睡5秒调用停止的方法。看看运行结果:

阻塞队列版生产消费模式

这就是阻塞队列版的生产消费模式,不用我们去控制线程的通信。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多