Java 中常见的阻塞队列有哪些?

Java 中常见的阻塞队列有哪些?

文章目录

  • Java 中常见的阻塞队列有哪些?
    • 前言
    • 项目环境
    • 1.ArrayBlockingQueue
    • 2.LinkedBlockingQueue
    • 3.SynchronousQueue
    • 4.PriorityBlockingQueue
    • 5.DelayQueue
    • 6.参考

前言

BlockingQueue 接口的实现类都在 J.U.C (java.util.concurrent)包中,本章将介绍以下 5 种常见的实现类

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • SynchronousQueue
  • PriorityBlockingQueue
  • DelayQueue

项目环境

  • jdk 1.8
  • github 地址:https://github.com/huajiexiewenfeng/java-concurrent
    • 本章模块:blockingqueue

1.ArrayBlockingQueue

ArrayBlockingQueue 是最典型的有界队列,其内部是用数组存储元素的,利用 ReentrantLock 实现线程安全,使用 Condition 来阻塞和唤醒线程

我们在创建它的时候就需要指定它的容量,之后也不可以再扩容了,在构造函数中我们同样可以指定是否是公平的,代码如下:

    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

第一个参数是容量,第二个参数是是否公平。和 ReentrantLock 一样,如果 ArrayBlockingQueue 被设置为非公平的,那么就存在插队的可能;如果设置为公平的,那么等待了最长时间的线程会被优先处理,其他线程不允许插队,不过这样的公平策略同时会带来一定的性能损耗,因为非公平的吞吐量通常会高于公平的情况。

2.LinkedBlockingQueue

从命名可以看出,这是一个内部用链表实现的 BlockingQueue。如果我们不指定它的初始容量,那么它容量默认就为整型的最大值 Integer.MAX_VALUE,由于这个数非常大,我们通常不可能放入这么多的数据,所以 LinkedBlockingQueue 也被称作无界队列,代表它几乎没有界限。

其他特点:

  • 同样也利用 ReentrantLock 实现线程安全,使用 Condition 来阻塞和唤醒线程
  • 无法设置 ReentrantLock 的公平非公平,默认是非公平
  • 也可以设置固定大小

默认无参构造函数如下,默认最大值 Integer.MAX_VALUE:

    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

3.SynchronousQueue

SynchronousQueue 最大的不同之处在于,它的容量为 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取。

需要注意的是,SynchronousQueue 的容量不是 1 而是 0,因为 SynchronousQueue 不需要去持有元素,它所做的就是直接传递(direct handoff)。由于每当需要传递的时候,SynchronousQueue 会把元素直接从生产者传给消费者,在此期间并不需要做存储,所以如果运用得当,它的效率是很高的。

为什么说它的容量是 0 ,我们可以看其中的几个方法:

  • peek 方法永远返回 null,代码如下:
    public E peek() {
        return null;
    }

因为 peek 方法的含义是取出头结点,但是 SynchronousQueue 的容量是 0,所以连头结点都没有,peek 方法也就没有意义,所以始终返回 null。

  • 同理,element 方法始终会抛出 NoSuchElementException 异常,但是这个方法的实现在它的父类 AbstractQueue 中
    public E element() {
        E x = peek();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }
  • SynchronousQueue 的 size 方法始终返回 0,因为它内部并没有容量,代码如下:
public int size() {
    return 0;
}

4.PriorityBlockingQueue

ArrayBlockingQueue 和 LinkedBlockingQueue 都是采用先进先出的顺序进行排序,可是如果有的时候我们需要自定义排序怎么办呢?这时就需要使用 PriorityBlockingQueue。

PriorityBlockingQueue 是一个支持优先级的无界阻塞队列,可以通过自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则。同时,插入队列的对象必须是可比较大小的,也就是 Comparable 的,否则会抛出 ClassCastException 异常。

带构 Comparator 参数的造函数如下:

    public PriorityBlockingQueue(int initialCapacity,
                                 Comparator<? super E> comparator) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        this.comparator = comparator;
        this.queue = new Object[initialCapacity];
    }

它的 take 方法在队列为空的时候会阻塞,但是正因为它是无界队列,而且会自动扩容,所以它的队列永远不会满,所以它的 put 方法永远不会阻塞,添加操作始终都会成功,也正因为如此,它的成员变量里只有一个 Condition:

private final Condition notEmpty;

这和之前的 ArrayBlockingQueue 拥有两个 Condition(分别是 notEmpty 和 notFull)形成了鲜明的对比,我们的 PriorityBlockingQueue 不需要 notFull,因为它永远都不会满。

示例

public class PriorityBlockingQueueDemo {
    public static void main(String[] args) {
        System.out.println("=====ArrayBlockingQueue=====");
        BlockingQueue<Node> arrayBlockingQueue = new ArrayBlockingQueue<>(10);
        arrayBlockingQueue.offer(new Node(2));
        arrayBlockingQueue.offer(new Node(3));
        arrayBlockingQueue.offer(new Node(1));
        for (int i = 0; i < 3; i++) {
            System.out.println(arrayBlockingQueue.poll().toString());
        }
        System.out.println("=====PriorityBlockingQueue=====");
        BlockingQueue<Node> priorityBlockingQueue = new PriorityBlockingQueue<>(10, new Node());
        priorityBlockingQueue.offer(new Node(2));
        priorityBlockingQueue.offer(new Node(3));
        priorityBlockingQueue.offer(new Node(1));
        for (int i = 0; i < 3; i++) {
            Node node = priorityBlockingQueue.poll();
            System.out.println(node.toString());
        }
    }

    static class Node implements Comparator<Node> {

        private Integer value;

        public Node() {
        }

        public Node(Integer value) {
            this.value = value;
        }

        public Integer getValue() {
            return value;
        }

        public void setValue(Integer value) {
            this.value = value;
        }

        @Override
        public int compare(Node o1, Node o2) {
            return o1.getValue() - o2.getValue();
        }

        @Override
        public String toString() {
            return "Node{" +
                    "value=" + value +
                    '}';
        }

    }

}

执行结果:

=====ArrayBlockingQueue=====
Node{value=2}
Node{value=3}
Node{value=1}
=====PriorityBlockingQueue=====
Node{value=1}
Node{value=2}
Node{value=3}

可以看到 PriorityBlockingQueue 队列取出的元素是经过排序的。

5.DelayQueue

DelayQueue 这个队列比较特殊,具有“延迟”的功能。我们可以设定让队列中的任务延迟多久之后执行,比如 10 秒钟之后执行,这在例如“30 分钟后未付款自动取消订单”等需要延迟执行的场景中被大量使用。

它是无界队列,放入的元素必须实现 Delayed 接口,而 Delayed 接口又继承了 Comparable 接口,所以自然就拥有了比较和排序的能力,Delayed 接口代码如下:

public interface Delayed extends Comparable<Delayed> {

    /**
     * Returns the remaining delay associated with this object, in the
     * given time unit.
     *
     * @param unit the time unit
     * @return the remaining delay; zero or negative values indicate
     * that the delay has already elapsed
     */
    long getDelay(TimeUnit unit);
}

可以看出这个 Delayed 接口继承自 Comparable。这里的 getDelay 方法返回的是“还剩下多长的延迟时间才会被执行”,如果返回 0 或者负数则代表任务已过期。元素会根据延迟时间的长短被放到队列的不同位置,越靠近队列头代表越早过期,DelayQueue 内部使用了 PriorityQueue 的能力来进行排序。

示例:

public class DelayQueueDemo {

    private static final long NANO_ORIGIN = System.nanoTime();

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<DelayedNode> delayQueue = new DelayQueue<>();
        delayQueue.offer(new DelayedNode(2, 4));
        delayQueue.offer(new DelayedNode(1, 2));
        for (int i = 0; i < 10; i++) {
            System.out.printf("时间[%d]\n", i + 1);
            Thread.sleep(1000);
            System.out.println(delayQueue.poll());
        }
    }

    static class DelayedNode implements Delayed {

        private Integer value;

        private long time;// 秒

        DelayedNode(Integer value, long time) {
            this.value = value;
            this.time = time;
        }

        public Integer getValue() {
            return value;
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return time - unit.toSeconds(delayTime());
        }

        private static long delayTime() {
            return System.nanoTime() - NANO_ORIGIN;
        }

        @Override
        public int compareTo(Delayed o) {
            if (o instanceof DelayedNode) {
                return value - ((DelayedNode) o).getValue();
            } else {
                return 0;
            }
        }

        @Override
        public String toString() {
            return "DelayedNode{" +
                    "value=" + value +
                    '}';
        }
    }
}

执行结果:

时间[1]
null
时间[2]
DelayedNode{value=1}
时间[3]
null
时间[4]
DelayedNode{value=2}
时间[5]
null

可以看到第 2 秒的时候,才取到第一个值 1,因为我们让值为 1 的元素,延迟 2 秒执行;同理第 4 秒的时候,我们取到值为 2 的元素。

6.参考

  • 《Java 并发编程 78 讲》- 徐隆曦

你可能感兴趣的:(Java并发编程)