18.线程系列- JUC中的阻塞队列

本文内容

  1. 掌握Queue,BlockingQueue接口中常用的方法
  2. 介绍6种阻塞队列,以及相关场景示例
  3. 重点掌握4种常用的阻塞队列

Queue接口

队列是一种先进先出FIFO的数据结构,java中用Queue接口来表示队列

Queue接口中定义了6个方法:

public interface Queue<E> extends Collection<E> {
    boolean add(e);
    boolean offer(E e);
    E remove();
    E poll();
    E element();
    E peek();
}

每个Queue方法都有两种形式:

  1. 如果操作失败则抛出异常
  2. 如果操作shiver,则返回特殊值(null或者false,具体取决于操作)

Queue从Collection继承的add方法插入一个元素,除非它违反了队列的容量限制,在这种情况下它会抛出IllegalStateException;offer方法与add不同之处仅在于它通过返回false来表示插入元素失败。

remove和poll方法都移除并返回队列的头部,确切的移除哪个元素是有具体的实现来决定的,仅当队列为空时,remove和poll方法的行为才有所不同,在这些情况下,remove抛出NoSuchElementException,而poll返回null。

element和peek方法返回队列头部的元素,但是不移除,它们之间的差异与move和poll的方法完全相同,如果队列为空,则element抛出NoSuchElementException,而peek返回null。

BlockingQueue接口

BlockingQueue位于juc中,熟称阻塞队列, 阻塞队列首先它是一个队列,继承Queue接口,是队列就会遵循先进先出(FIFO)的原则,又因为它是阻塞的,故与普通的队列有两点区别:

  1. 当一个线程向队列里面添加数据时,如果队列是满的,那么将阻塞该线程,暂停添加数据
  2. 当一个线程从队列里面取出数据时,如果队列是空的,那么将阻塞该线程,暂停取出数据

相关方法:

 重点,再来解释一下,加深印象:

  1. 3个可能会有异常的方法,add、remove、element;这3个方法不会阻塞(是说队列满或者空的情况下是否会阻塞);队列满的情况下,add抛出异常;队列为空情况下,remove、element抛出异常
  2. offer、poll、peek 也不会阻塞(是说队列满或者空的情况下是否会阻塞);队列满的情况下,offer返回false;队列为空的情况下,pool、peek返回null
  3. 队列满的情况下,调用put方法会导致当前线程阻塞
  4. 队列为空的情况下,调用take方法会导致当前线程阻塞
  5. offer(e,timeuout,unit),超时之前,插入成功返回true,否者返回false
  6. poll(timeout,unit),超时之前,获取到头部元素并将其移除,返回true,否者返回false

BlockingQueue常见的实现类

ArrayBlockingQueue

基于数组的阻塞队列实现,其内部维护一个定长的数组,用于存储队列元素。线程阻塞的实现是通过ReentrantLock来完成的,数据的插入与取出共用同一个锁,因此ArrayBlockingQueue并不能实现生产、消费同时进行。而且在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。

 LinkedBlockingQueue

基于单向链表的阻塞队列实现,在初始化LinkedBlockingQueue的时候可以指定大小,也可以不指定,默认类似一个无限大小的容量(Integer.MAX_VALUE),不指队列容量大小也是会有风险的,一旦数据生产速度大于消费速度,系统内存将有可能被消耗殆尽,因此要谨慎操作。另外LinkedBlockingQueue中用于阻塞生产者、消费者的锁是两个(锁分离),因此生产与消费是可以同时进行的。

 PriorityBlockingQueue

一个支持优先级排序的无界阻塞队列,进入队列的元素会按照优先级进行排序

 SynchronousQueue

同步阻塞队列,SynchronousQueue没有容量,与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue,每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然

DelayQueue

 DelayQueue是一个支持延时获取元素的无界阻塞队列,里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素,如果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行,也就是说只有在延迟期到时才能够从队列中取元素

 LinkedTransferQueue

LinkedTransferQueue是基于链表的FIFO无界阻塞队列,它出现在JDK7中,Doug Lea 大神说LinkedTransferQueue是一个聪明的队列,它是ConcurrentLinkedQueue、SynchronousQueue(公平模式下)、无界的LinkedBlockingQueues等的超集,LinkedTransferQueue包含了ConcurrentLinkedQueue、SynchronousQueue、LinkedBlockingQueues三种队列的功能

 ArrayBlockingQueue

有界阻塞队列,内部使用数组存储元素,有2个常用构造方法:

//capacity表示容量大小,默认内部采用非公平锁
public ArrayBlockingQueue(int capacity)
//capacity:容量大小,fair:内部是否是使用公平锁
public ArrayBlockingQueue(int capacity, boolean fair)

需求:业务系统中有很多地方需要推送通知,由于需要推送的数据太多,我们将需要推送的消息先丢到阻塞队列中,然后开一个线程进行处理真实发送,代码如下:

public class ArrayBlockingQueueTest {

    //定义队列
    static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(100);

    static {
        //启动一个线程做真实推送
        new Thread(() -> {
            while (true) {
                String msg;
                try {
                    long starTime = System.currentTimeMillis();
                    //获取一条推送消息,此方法会进行阻塞,直到返回结果
                    msg = (String) blockingQueue.take();
                    long endTime = System.currentTimeMillis();
                    //模拟推送耗时
                    TimeUnit.MILLISECONDS.sleep(500);

                    System.out.println(String.format("[%s,%s,take耗时:%s],%s,发送消息:%s", starTime, endTime, (endTime - starTime), Thread.currentThread().getName(), msg));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    //定义消息存放队列方法
    static void put(String msg) throws InterruptedException {
        blockingQueue.put(msg);
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            put("消息:" + i);
        }
    }
}

输出:

[1608521393385,1608521393385,take耗时:0],Thread-0,发送消息:消息:0
[1608521393922,1608521393922,take耗时:0],Thread-0,发送消息:消息:1
[1608521394425,1608521394425,take耗时:0],Thread-0,发送消息:消息:2
[1608521394926,1608521394926,take耗时:0],Thread-0,发送消息:消息:3
[1608521395426,1608521395426,take耗时:0],Thread-0,发送消息:消息:4
[1608521395926,1608521395926,take耗时:0],Thread-0,发送消息:消息:5
[1608521396427,1608521396427,take耗时:0],Thread-0,发送消息:消息:6
[1608521396928,1608521396928,take耗时:0],Thread-0,发送消息:消息:7
[1608521397428,1608521397428,take耗时:0],Thread-0,发送消息:消息:8
[1608521397928,1608521397928,take耗时:0],Thread-0,发送消息:消息:9

代码中我们使用了有界队列ArrayBlockingQueue,创建ArrayBlockingQueue时候需要制定容量大小,调用blockingQueue.put将推送信息放入队列中,如果队列已满,此方法会阻塞。代码中在静态块中启动了一个线程,调用blockingQueue.take();从队列中获取待推送的信息进行推送处理。

注意:ArrayBlockingQueue如果队列容量设置的太小,消费者发送的太快,消费者消费的太慢的情况下,会导致队列空间满,调用put方法会导致发送者线程阻塞,所以注意设置合理的大小,协调好消费者的速度。

LinkedBlockingQueue

内部使用单向链表实现的阻塞队列,3个构造方法

//默认构造方法,容量大小为Integer.MAX_VALUE
public LinkedBlockingQueue();
//创建指定容量大小的LinkedBlockingQueue
public LinkedBlockingQueue(int capacity);
//容量为Integer.MAX_VALUE,并将传入的集合丢入队列中
public LinkedBlockingQueue(Collection<? extends E> c);

LinkedBlockingQueue的用法和ArrayBlockingQueue累死,建议使用的时候指定容量,如果不指定容量,插入的太快,移除的太慢,可能会产生OOM

PriorityBolckingQueue

无界的优先级阻塞队列,内部使用数组存储数据,达到容量时,会自动进行扩容,放入的元素会按照优先级进行排序,4个构造方法:

//默认构造方法,默认初始化容量是11
public PriorityBlockingQueue();
//指定队列的初始化容量
public PriorityBlockingQueue(int initialCapacity);
//指定队列的初始化容量和放入元素的比较器
public PriorityBlockingQueue(int initialCapacity,Comparator<? super E> comparator);
//传入集合放入来初始化队列,传入的集合可以实现SortedSet接口或者PriorityQueue接口进行排序,如果没有实现这2个接口,按正常顺序放入队列
public PriorityBlockingQueue(Collection<? extends E> c);

优先级队列放入元素的时候,会进行排序,所以我们需要制定排序规则,有2种方式:

  1. 创建PriorityBlockingQueue制定比较器Comparator
  2. 放入的元素需要实现Comparable接口

需求:还是上面的推送业务,目前推送是按照放入的先后顺序进行发送的,比如有些公告比较紧急,优先级比较高,需要快点发送,怎么搞?此时PriorityBlockingQueue就派上用场了,代码如下:

public class PriorityBlockingQueueTest {

    //定义队列
    static PriorityBlockingQueue blockingQueue = new PriorityBlockingQueue(100);

    static {
        //启动一个线程做真实推送
        new Thread(() -> {
            while (true) {
                Msg msg;
                try {
                    long starTime = System.currentTimeMillis();
                    //获取一条推送消息,此方法会进行阻塞,直到返回结果
                    msg = (Msg) blockingQueue.take();
                    long endTime = System.currentTimeMillis();
                    //模拟推送耗时
                    TimeUnit.MILLISECONDS.sleep(500);

                    System.out.println(String.format("[%s,%s,take耗时:%s],%s,发送消息:%s", starTime, endTime, (endTime - starTime), Thread.currentThread().getName(), msg.getMsg()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    //定义消息存放队列方法
    static void put(Msg msg) throws InterruptedException {
        blockingQueue.put(msg);
    }

    static class Msg implements Comparable<Msg> {

        //优先级
        private int priority;
        //消息
        private String msg;

        Msg(int priority, String msg) {
            this.priority = priority;
            this.msg = msg;
        }

        @Override
        public int compareTo(Msg msg) {
            return Integer.compare(msg.getPriority(), priority);
        }

        public int getPriority() {
            return priority;
        }

        public void setPriority(int priority) {
            this.priority = priority;
        }

        public String getMsg() {
            return msg;
        }

        public void setMsg(String msg) {
            this.msg = msg;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i < 10; i++) {
            put(new Msg(i, "消息:" + i));
        }
    }
}

输出:

[1608528186767,1608528186769,take耗时:2],Thread-0,发送消息:消息:9
[1608528187305,1608528187305,take耗时:0],Thread-0,发送消息:消息:8
[1608528187806,1608528187806,take耗时:0],Thread-0,发送消息:消息:7
[1608528188306,1608528188306,take耗时:0],Thread-0,发送消息:消息:6
[1608528188807,1608528188807,take耗时:0],Thread-0,发送消息:消息:5
[1608528189307,1608528189307,take耗时:0],Thread-0,发送消息:消息:4
[1608528189808,1608528189808,take耗时:0],Thread-0,发送消息:消息:3
[1608528190309,1608528190309,take耗时:0],Thread-0,发送消息:消息:2
[1608528190809,1608528190809,take耗时:0],Thread-0,发送消息:消息:1

SynchronousQueue

同步阻塞队列,SynchronousQueue没有容量,与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue,每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。SynchronousQueue 在现实中用的不多,线程池中有用到过,Executors.newCachedThreadPool()实现中用到了这个队列,当有任务丢入线程池的时候,如果已创建的工作线程都在忙于处理任务,则会新建一个线程来处理丢入队列的任务。

 

public class SynchronousQueueTest {

    static SynchronousQueue<String> queue = new SynchronousQueue<>();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            try {
                long starTime = System.currentTimeMillis();
                queue.put("java高并发系列!");
                long endTime = System.currentTimeMillis();
                System.out.println(String.format("[%s,%s,take耗时:%s],%s", starTime, endTime, (endTime - starTime), Thread.currentThread().getName()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        //休眠5秒之后,从队列中take一个元素
        TimeUnit.SECONDS.sleep(5);
        System.out.println(System.currentTimeMillis() + "调用take获取并移除元素," + queue.take());
    }
}

输出:

1608528731136调用take获取并移除元素,java高并发系列!
[1608528726135,1608528731136,take耗时:5001],Thread-0

main方法中启动了一个线程,调用queue.put方法向队列中丢入一条数据,调用的时候产生了阻塞,从输出结果中可以看出,直到take方法被调用时,put方法才从阻塞状态恢复正常。

DelayQueue

DelayQueue是一个支持延时获取元素的无界阻塞队列,里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素,如果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行,也就是说只有在延迟期到时才能够从队列中取元素。

 需求:还是推送业务,有时候我们希望早上9点或者其他时间进行推送,如何实现呢?此时DelayQueue就派上用场了

我们先看一下类的声明:

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E>

元素E需要实现接口Delay,我们看一下这个接口的代码:

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}

Delayed继承了Comparable接口,这个接口使用来做比较用的,DelayQueue内部使用了PriorityQueue来存储数据的,PriorityQueue是一个优先级队列,丢入的数据会进行排序,排序的方法调用的是Comparable接口中的方法。主要说一下Delayed接口中的geyDelay方法:此方法在给定的时间单位内返回与此对象关联的剩余延迟时间。

对推送我们在做一下处理,让其支持定时发送:

public class DelayQueueTest {

    static DelayQueue<Msg> pushQueue = new DelayQueue<>();

    static {
        //启动一个线程做真实推送
        new Thread(() -> {
            while (true) {
                Msg msg;
                try {
                    //获取一条推送消息,此方法会进行阻塞,直到返回结果
                    msg = pushQueue.take();
                    //此处可以做真实推送
                    long endTime = System.currentTimeMillis();
                    System.out.println(String.format("定时发送时间:%s,实际发送时间:%s,发送消息:%s", msg.sendTimeMs, endTime, msg));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    //推送消息,需要发送推送消息的调用该方法,会将推送信息先加入推送队列
    public static void pushMsg(int priority, String msg, long sendTimeMs) throws InterruptedException {
        pushQueue.put(new Msg(priority, msg, sendTimeMs));
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 5; i >= 1; i--) {
            String msg = "一起来学java高并发,第" + i + "天";
            pushMsg(i, msg, Calendar.getInstance().getTimeInMillis() + i * 2000);
        }
    }

    static class Msg implements Delayed {

        //优先级
        private int priority;
        //消息
        private String msg;
        //发送毫秒数
        private long sendTimeMs;

        Msg(int priority, String msg, long sendTimeMs) {
            this.priority = priority;
            this.msg = msg;
            this.sendTimeMs = sendTimeMs;
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(this.sendTimeMs - Calendar.getInstance().getTimeInMillis(),
                    TimeUnit.MILLISECONDS);
        }

        @Override
        public int compareTo(Delayed o) {
            if (o instanceof Msg) {
                Msg c2 = (Msg) o;
                return Integer.compare(this.priority, c2.priority);
            }
            return 0;
        }

        @Override
        public String toString() {
            return "Msg{" +
                    "priority=" + priority +
                    ", msg='" + msg + '\'' +
                    ", sendTimeMs=" + sendTimeMs +
                    '}';
        }
    }
}

输出:

定时发送时间:1608529641019,实际发送时间:1608529641021,发送消息:Msg{priority=1, msg='一起来学java高并发,第1天', sendTimeMs=1608529641019}
定时发送时间:1608529643018,实际发送时间:1608529643019,发送消息:Msg{priority=2, msg='一起来学java高并发,第2天', sendTimeMs=1608529643018}
定时发送时间:1608529645018,实际发送时间:1608529645020,发送消息:Msg{priority=3, msg='一起来学java高并发,第3天', sendTimeMs=1608529645018}
定时发送时间:1608529647017,实际发送时间:1608529647019,发送消息:Msg{priority=4, msg='一起来学java高并发,第4天', sendTimeMs=1608529647017}
定时发送时间:1608529648989,实际发送时间:1608529648990,发送消息:Msg{priority=5, msg='一起来学java高并发,第5天', sendTimeMs=1608529648989}

可以看出时间发送时间,和定时发送时间基本一致,代码中Msg需要实现Delayed接口,重点在于getDelay方法,这个方法返回剩余的延迟时间,代码中使用this.sendTimeMs减去当前时间的毫秒格式时间,得到剩余延迟时间。

LinkedTransferQueue

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

 LinkedTransferQueue类继承自AbstractQueue抽象类,并且实现了TransferQueue接口:

public interface TransferQueue<E> extends BlockingQueue<E> {
    // 如果存在一个消费者已经等待接收它,则立即传送指定的元素,否则返回false,并且不进入队列。
    boolean tryTransfer(E e);
    // 如果存在一个消费者已经等待接收它,则立即传送指定的元素,否则等待直到元素被消费者接收。
    void transfer(E e) throws InterruptedException;
    // 在上述方法的基础上设置超时时间
    boolean tryTransfer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;
    // 如果至少有一位消费者在等待,则返回true
    boolean hasWaitingConsumer();
    // 获取所有等待获取元素的消费线程数量
    int getWaitingConsumerCount();
}

再看一下上面的这些方法,transfer(E e)方法和SynchronousQueue的put方法类似,都需要等待消费者取走元素,否者一直等待。其他方法和ArrayBlockingQueue、LinkedBlockingQueue中的方法类似。

总结

  1. 重点了解BlockingQueue中的所有方法,以及他们的区别
  2. 重点掌握ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue,DelayQueue的使用场景
  3. 需要处理任务的优先级的,使用PriorityBlockingQueue
  4. 处理任务需要延时处理的,使用DelayQueue
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值