JUC基础08——阻塞队列BlockingQueue

38 阅读9分钟

阻塞队列BlockingQueue

什么是队列

队列是一种遵循先进先出(First-In-First-Out,FIFO)原则的数据结构。简单来说,队列就像是排队等待服务的人群,新来的人总是排在队尾,而最先到达的人则排在队首。
队列有两个基本操作:入队(enqueue)和出队(dequeue)。入队操作将元素添加到队列的末尾,而出队操作则从队列的首部移除元素。这样就保证了先入队的元素会先出队,符合FIFO的规则

queue.png

阻塞队列-BlockingQueue

简述

阻塞队列(BlockingQueue)是位于 java.util.concurrent 包下,它是一种线程安全的队列,支持在队列为空时阻塞取队列元素的操作,以及在队列满时往阻塞队列中添加元素的操作。阻塞队列常用于生产者和消费者的场景中进行线程间通信。

阻塞队列的特点

阻塞队列的特点如下:

  1. 线程安全:阻塞队列的所有操作都是线程安全的,可以在多线程环境中直接使用
  2. 阻塞操作:阻塞队列的取操作和添操作可以在特定条件下进行阻塞。如果队列为空, 操作会阻塞直到队列不为空;如果队列已满, 操作会阻塞到直到队列不为满
  3. 容量限制:阻塞队列的容量通常是固定的,但也可以是动态的。如果是动态的,队列的容量可以根据需要自动扩展或缩小,如ArrayBlockingQueue初始化时需要指定容量,LinkedBlockingQueue则默认上限容量为 Integer.MAX_VALUE,也可以初始化时指定容量。
  4. 优先级:阻塞队列通常没有提供优先级控制,所有元素都是先进先出(First-In-First-Out,FIFO)的。。但是,一些实现类(如PriorityBlockingQueue)提供了基于优先级的排序功能
  5. 并发访问:阻塞队列支持多个线程同时进行取和添加操作。
  6. 失败策略:阻塞队列的操作通常会返回一个结果,如果操作失败,通常抛出异常或返回特殊值

阻塞队列接口的实现

BlockingQueue阻塞队列是属于一个接口,底下有七个实现类

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

image.png

核心方法

方法类型抛出异常特殊值阻塞超时
插入add(e)offer()put(e)offer(e,time,unit)
移除remove()poll()take()poll(time,unit)
检查element()peek不可用不可用
  • 抛出异常:当阻塞队列满时:在队列中add插入元素会抛出 java.lang.IllegalStateException: Queue full ;当阻塞队列空时:再往队列中 remove 元素,会抛出 java.util.NoSuchElementException

  • 特殊值:插入方法,成功true,失败false 移除方法:成功返回出队列元素,队列没有就返回空

  • 一直阻塞:当阻塞队列满时,生产者继续往队列里 put 元素,队列会一直阻塞生产线程直到 put 数据响应中断退出,当阻塞队列空时,消费者线程试图从队列里 take 元素,队列uhi一直阻塞消费者线程直到队列可用

  • 超时退出:当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出

阻塞队列核心方法使用案例

此处以 ArrayBlockingQueue 为例

抛出异常组

添加元素
执行add方法,向已经满的ArrayBlockingQueue中添加元素时候,会抛出异常

public static void main(String[] args) {
    BlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(arrayBlockingQueue.add(1));
    System.out.println(arrayBlockingQueue.add(2));
    System.out.println(arrayBlockingQueue.add(3));
    //在达到容量之后继续添加
    System.out.println(arrayBlockingQueue.add(4));
}

执行结果

true
true
true
Exception in thread "main" java.lang.IllegalStateException: Queue full
	at java.util.AbstractQueue.add(AbstractQueue.java:98)
	at java.util.concurrent.ArrayBlockingQueue.add(ArrayBlockingQueue.java:312)
	at com.avgrado.demo.thread.BlockingQueueDemo.main(BlockingQueueDemo.java:19)

取出元素
同时如果我们多取出元素的时候,也会抛出异常,我们只存储了3个值,但是取的时候,取了四次

public static void main(String[] args) {
    BlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(arrayBlockingQueue.add(1));
    System.out.println(arrayBlockingQueue.add(2));
    System.out.println(arrayBlockingQueue.add(3));
    
    System.out.println(arrayBlockingQueue.remove());
    System.out.println(arrayBlockingQueue.remove());
    System.out.println(arrayBlockingQueue.remove());
    //队列为空后继续取出
    System.out.println(arrayBlockingQueue.remove());
}

执行结果

true
true
true
1
2
3
Exception in thread "main" java.util.NoSuchElementException
	at java.util.AbstractQueue.remove(AbstractQueue.java:117)
	at com.avgrado.demo.thread.BlockingQueueDemo.main(BlockingQueueDemo.java:23)

布尔类型组

添加元素使用 offer 方法,如果阻塞队列满了后,会返回false 取出元素使用 poll 方法,如果队列已空,那么会返回null

public static void main(String[] args) {
    BlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(arrayBlockingQueue.offer(1));
    System.out.println(arrayBlockingQueue.offer(2));
    System.out.println(arrayBlockingQueue.offer(3));

    //达到容量之后继续添加
    System.out.println(arrayBlockingQueue.offer(4));

    System.out.println(arrayBlockingQueue.poll());
    System.out.println(arrayBlockingQueue.poll());
    System.out.println(arrayBlockingQueue.poll());
    //队列为空后继续取出
    System.out.println(arrayBlockingQueue.poll());
}

执行结果

true
true
true
false
1
2
3
null

阻塞组

使用 put 方法,添加元素时候,如果阻塞队列满了后,添加消息的线程,会一直阻塞,直到队列元素减少到队列容量以下,才会唤醒 使用take 方法取消息的时候,如果阻塞队列为空,获取消息的线程会一直阻塞,直到队列不为空,才会唤醒

添加元素

class QueueDemo{
    BlockingQueue blockingQueue = new ArrayBlockingQueue(2);

    public void putEle() throws Exception {
        System.out.println(Thread.currentThread().getName()+"\t 第一次添加元素");
        blockingQueue.put("1");
        blockingQueue.put("2");
        System.out.println(Thread.currentThread().getName()+"\t =====");
        blockingQueue.put("3");
        System.out.println(Thread.currentThread().getName()+"\t 第二次添加元素");
    }

    public void takeEle() throws Exception {
        System.out.println(Thread.currentThread().getName()+"\t 准备取出元素");
        Object takeValue = blockingQueue.take();
        System.out.println(Thread.currentThread().getName()+"\t 取出的元素是"+takeValue);
    }
}
/**
 * @ClassName BlockingQueueDemo
 */
public class BlockingQueueDemo {
    public static void main(String[] args) throws Exception {
        QueueDemo queueDemo = new QueueDemo();
        new Thread(()->{
            try {

            } catch (Exception e) {
                e.printStackTrace();
            }
        },"A").start();
        //睡5秒,保证A线程添加元素全部执行,并能展示阻塞效果
        TimeUnit.SECONDS.sleep(5);
        new Thread(()->{
            try {
                queueDemo.takeEle();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"B").start();
    }

执行结果:

b.gif

取出元素

class QueueDemo{
    BlockingQueue blockingQueue = new ArrayBlockingQueue(2);

    public void putEle() throws Exception {
        System.out.println(Thread.currentThread().getName()+"\t 添加元素");
        blockingQueue.put("1");
        System.out.println(Thread.currentThread().getName()+"\t 添加元素完成");

    }

    public void takeEle() throws Exception {
        System.out.println(Thread.currentThread().getName()+"\t 准备取出元素");
        Object takeValue = blockingQueue.take();
        System.out.println(Thread.currentThread().getName()+"\t 取出的元素是"+takeValue);
    }
}
/**
 * @ClassName BlockingQueueDemo
 */
public class BlockingQueueDemo {
    public static void main(String[] args) throws Exception {
        QueueDemo queueDemo = new QueueDemo();
        new Thread(()->{
            try {
                queueDemo.takeEle();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"A").start();
        //睡5秒,保证A线程先执行取出元素,并能展示阻塞效果
        TimeUnit.SECONDS.sleep(5);
        new Thread(()->{
            try {
                queueDemo.putEle();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"B").start();
    }
}

执行结果:

c.gif

超时时间组

offer() 和 poll() 方法加上时间参数

使用offer插入的时候,需要指定时间,如果2秒还没有插入,那么就放弃插入: 使用poll取出的时候,需要指定时间,如果2秒还没有取出,那么就放弃取出

public static void main(String[] args) throws Exception {
    BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
    System.out.println(blockingQueue.offer("b", 2L, TimeUnit.SECONDS));
    System.out.println(blockingQueue.offer("c", 2L, TimeUnit.SECONDS));
    System.out.println(blockingQueue.offer("d", 2L, TimeUnit.SECONDS));

    System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
    System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
    System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
    System.out.println(blockingQueue.poll(2L, TimeUnit.SECONDS));
}

执行结果

true
true
true
false
a
b
c
null

不存储元素的 SynchronousQueue

SynchronousQueue没有容量,与其他BlockingQueue不同,SynchronousQueue是一个不存储的BlockingQueue,每一个put操作必须等待一个take操作,否者不能继续添加元素

代码示例:

public static void main(String[] args) {
    BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();
    new Thread(()->{
        try{
            System.out.println(Thread.currentThread().getName()+"\t put A");
            synchronousQueue.put("A");

            System.out.println(Thread.currentThread().getName()+"\t put B");
            synchronousQueue.put("B");

            System.out.println(Thread.currentThread().getName()+"\t put C");
            synchronousQueue.put("C");
        }catch (Exception e){
            e.printStackTrace();
        }

    },"t1").start();

    
    new Thread(() -> {
        try {

            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronousQueue.take();
            System.out.println(Thread.currentThread().getName() + "\t take A ");

            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronousQueue.take();
            System.out.println(Thread.currentThread().getName() + "\t take B ");

            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronousQueue.take();
            System.out.println(Thread.currentThread().getName() + "\t take C ");

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "t2").start();

}

执行结果:

t1	 put A

5秒后...
t2	 take A 
t1	 put B

5秒后...
t2	 take B 
t1	 put C

5秒后...
t2	 take C 

最后的运行结果可以看出,每次t1线程向队列中添加阻塞队列添加元素后,t1输入线程就会等待 t2消费线程,t2消费后,t2处于挂起状态,等待t1在存入,从而周而复始,形成 一存一取的状态

阻塞队列的用处

生产者消费者模式

1.使用传统的 Lock 实现简易版本的生产者-消费者模式 :对一个初始值为0的变量,两个线程对其交替操作,一个加1,一个减1,来5轮


/**
 * 定义资源类
 */
class ShareData{
    private int num = 0;
    private Lock lock = new ReentrantLock();

    Condition condition = lock.newCondition();

    /**
     * 自增:生产
     * @throws Exception
     */
    public void increment() throws Exception{

        lock.lock();
        try{
            while (num !=0){
                condition.await();
            }

            num ++;

            System.out.println(Thread.currentThread().getName()+"\t "+num);

            //通知唤醒
            condition.signalAll();
        }finally {
            lock.unlock();
        }

    }

     /**
     * 自减:消费
     * @throws Exception
     */
    public void decrement() throws  Exception{
        lock.lock();
        try{
            while (num ==0){
                condition.await();
            }

            num --;

            System.out.println(Thread.currentThread().getName()+"\t "+num);

            //通知唤醒
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }
}


/**
 * @ClassName ProdConsumerTraditionDemo
 * @Description TODO
 * @Author gongchen
 * @Date 2023-11-20 11:15
 */
public class ProdConsumerTraditionDemo {
    public static void main(String[] args) {
        ShareData shareData = new ShareData();
        // t1线程,生产
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    shareData.increment();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "生产线程:t1").start();

        // t2线程,消费
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    shareData.decrement();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "消费线程:t2").start();
    }
}

执行结果:

生产线程:t1	 1
消费线程:t2	 0
生产线程:t1	 1
消费线程:t2	 0
生产线程:t1	 1
消费线程:t2	 0
生产线程:t1	 1
消费线程:t2	 0
生产线程:t1	 1
消费线程:t2	 0

使用:volatile、CAS、atomicInteger、BlockQueue、线程交互、原子引用 完成阻塞队列版生产者和消费者示例

class MyResoure{

    // 默认开启,进行生产消费
    // 这里用到了volatile是为了保持数据的可见性,也就是当 FLAG 修改时,要马上通知其它线程进行修改
    private volatile  Boolean FLAG = true;

    //使用原子包装类
    private  AtomicInteger atomicInteger = new AtomicInteger();

    //定义一个 未实现的 BlockingQueue ,在构造方法中从传入的实现类中进行实例化
    BlockingQueue<String> blockingQueue = null;

    //而应该采用依赖注入里面的,构造注入方法传入
    public MyResoure (BlockingQueue<String> blockingQueue){
        this.blockingQueue = blockingQueue;
        // 查询出传入的class是什么
        System.out.println(Thread.currentThread().getName());
    }

    /**
     * 生产
     * @throws Exception
     */
    public void produce() throws Exception{
        String data = "";
        boolean retValue ;
        // 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
        // 当FLAG为true的时候,开始生产
        while (FLAG){
            data= atomicInteger.incrementAndGet()+"";
            retValue = blockingQueue.offer(data,2L, TimeUnit.SECONDS);
            if(retValue){
                System.out.println(Thread.currentThread().getName()+"\t 插入队列"+ data + "成功");
            }else{
                System.out.println(Thread.currentThread().getName()+"\t 插入队列"+ data + "失败");
            }

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "\t 停止生产,表示FLAG=false,生产结束");
    }

    /**
     * 消费
     * @throws Exception
     */
    public void consume() throws  Exception{
        String retValue ;
        // 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
        // 当FLAG为true的时候,开始生产
        while (FLAG){
            retValue =  blockingQueue.poll(2L,TimeUnit.SECONDS);
            if(retValue!=null && retValue != ""){
                System.out.println(Thread.currentThread().getName()+"\t 消费队列:"+retValue +"成功");
            }else{
                FLAG = false;
                System.out.println(Thread.currentThread().getName()+"\t 消费失败,队列中已为空,退出");

                //退出消费队列
                return;
            }
        }
    }

    /**
     * 停止生产的判断
     */
    public void stop() {
        this.FLAG = false;
    }
}



/**
 * @ClassName ProdConsumerTraditionDemo
 * @Description TODO
 * @Author gongchen
 * @Date 2023-11-20 11:15
 */
public class ProdConsumerTraditionDemo {
    public static void main(String[] args) {
        //传入具体的实现类: ArrayBlockingQueue
        MyResoure  resoure = new MyResoure(new ArrayBlockingQueue<>(10));
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "\t 生产线程启动");
            try {
                resoure.produce();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"生产者T1").start();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "\t 消费线程启动");
            try {
                resoure.consume();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"消费者T2").start();

        // 5秒后,停止生产和消费
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("");
        System.out.println("");
        System.out.println("5秒中后,生产和消费线程停止,线程结束");
        resoure.stop();
    }
}