Java基础 : BlockingQueue浅析

文章目录

  • 一、前言
    • 1. 简介
    • 2. 分类
    • 3. 关键方法
  • 二、源码分析
    • 1. SynchronousQueue
      • 1.1 介绍
      • 1.2 使用场景举例
    • 2. LinkedBlockingDeque
      • 2.1 入队
      • 2.2 出队
    • 3. DelayQueue
      • 3.1 PriorityQueue
      • 3.2 关键方法

一、前言

本文仅仅是对 BlockingQueue 的种类和方法进行简单介绍,对于部分实现进行了简单的代码分析。

1. 简介

BlockingQueue 即阻塞队列,关于阻塞队列的介绍已经有很多文章,因此这里直接借用 Java阻塞队列 一文中的介绍。如下:
阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下图所示:
Java基础 : BlockingQueue浅析_第1张图片

  • 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
  • 当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。

  • 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。
  • 试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从列中移除一个或者多个元素或者完全清空队列后使队列重新变得空闲起来并后续新增。

2. 分类

BlockingQueue 的类结构如下:
Java基础 : BlockingQueue浅析_第2张图片

BlockingQueue 只是一个接口,Jdk 提供了多种实现类,如下:

实现类 特性
ArrayBlockingQueue 由数组结构组成的有界阻塞队列
LinkedBlockingQueue 由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列
SynchronousQueue 不存储元素的阻塞队列,也即单个元素的队列
PriorityBlockingQueue 支持优先级排序的无界阻塞队列
DelayQueue 使用优先级队列实现的延迟无界阻塞队列
LinkedTransferQueue 由链表结构组成的无界阻塞队列
LikedBlockingDeque 由链表结构组成的双向阻塞队列

3. 关键方法

关键方法如下:

方法类型 方法名 释义
入队操作 add(e) 入队成功返回 true,若队列已满则抛出异常
入队操作 offer(e) 入队成功返回 true ,否则false
入队操作 put(e) 队列满时入队则会一直阻塞线程,直至入队成功
入队操作 offer(e, time, unit) 将指定元素插入此队列,如果队列已满,则等待指定的等待时间。
出队操作 remove() 出队操作,返回出队元素,如果队列为空则抛出异常
出队操作 poll() 出队操作,如果队列为空则返回 null,否则返回出队元素
出队操作 take() 出队操作,如果队列为空则阻塞线程,直至出队成功
出队操作 poll(time, unit) 检索并删除此队列的头部,如果有必要等待指定的等待时间以使元素可用。
检查操作 element() 检索但不删除此队列的头部。此方法与peek的不同之处仅在于如果此队列为空,它将引发异常。
检查操作 peek() 检索但不删除此队列的头部,如果此队列为空,则返回null 。

二、源码分析

BlockingQueue 的使用我们这里就不再赘述,这里来简单看看其中部分代码实现。我们调其中几个实现来简单看一下:

1. SynchronousQueue

1.1 介绍

SynchronousQueue 是不存储元素的阻塞队列,也即单个元素的队列。

简单来说 :当我们调用 入队方法(put、add、 offer) 时并不会立刻返回,而是阻塞等待,直到有其他操作(一般是其他线程)调用了该队列的出队方法 (remove、poll、take) 后,入队方法才会返回结果。同理当我们调用出队方法时如果之前没有其他操作调用了入队方法则会挂起等待,直至其他操作调用入队方法。

关于SynchronousQueue 的源码,本文就不做分析了,如果需要详参 老猿说说-SynchronousQueue


1.2 使用场景举例

在Executors#newCachedThreadPool 中使用了 SynchronousQueue 作为默认的任务队列,如下:

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

这里可以看到,新建的线程池的最大线程数量是 Integer.MAX_VALUE,当任务数量一瞬间大于 Integer.MAX_VALUE 时(想想也不太可能,如果能到达应该做其他优化了)会将多余的任务放置到任务队列 SynchronousQueue中,此时会挂起线程,直至有其他线程消化了任务开始从队列中获取任务时,入队任务才会有返回。


实际上, SynchronousQueue 的使用场景在其官方注释上已经说明 :它们非常适合切换设计,其中在一个线程中运行的对象必须与在另一个线程中运行的对象同步,以便将一些信息、事件或任务交给它。但是我目前没碰到需要使用的情况

2. LinkedBlockingDeque

LinkedBlockingDeque 是由链表结构组成的双向阻塞队列 。LinkedBlockingDeque 针对入队和出队方法都有对应的插入队头队尾的方法,

如入队方法:

  • LinkedBlockingDeque#add : 默认插入队尾
  • LinkedBlockingDeque#addFirst :插入队头
  • LinkedBlockingDeque#addLast :插入队尾

如出队方法:

  • LinkedBlockingDeque#take : 默认队头出队
  • LinkedBlockingDeque#takeFirst:队头出队
  • LinkedBlockingDeque#takeLast:队尾出队

下面我们来具体看一看实现:

2.1 入队

由于 LinkedBlockingDeque#put 和 LinkedBlockingDeque#offer 和 LinkedBlockingDeque#add 的实现基本相同如下,所以下面我们以LinkedBlockingDeque#add 为例

  1. LinkedBlockingDeque#add

    	// 默认入队队尾
        public boolean add(E e) {
        	// 添加到队列尾部
            addLast(e);
            return true;
        }
        // 入队队头
        public void addFirst(E e) {
            if (!offerFirst(e))
                throw new IllegalStateException("Deque full");
        }
        // 入队队尾
        public void addLast(E e) {
        	// 如果队列已满则抛出异常
            if (!offerLast(e))
                throw new IllegalStateException("Deque full");
        }
        
        // 队头入队
    	public boolean offerFirst(E e) {
            if (e == null) throw new NullPointerException();
            // 初始化当前元素节点
            Node<E> node = new Node<E>(e);
            // 加锁
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
            	// 入队
                return linkFirst(node);
            } finally {
                lock.unlock();
            }
        }
        
        public boolean offerLast(E e) {
            if (e == null) throw new NullPointerException();
            // 构建节点
            Node<E> node = new Node<E>(e);
            // 加锁
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                return linkLast(node);
            } finally {
                lock.unlock();
            }
        }
    

    上面可以看到,关键的代码在于 LinkedBlockingDeque#linkFirst 和LinkedBlockingDeque#linkLast 中,具体实现如下:

        /**
         * 将节点链接为最后一个元素,如果已满则返回 false。
         */
        private boolean linkLast(Node<E> node) {
            // assert lock.isHeldByCurrentThread();
            // 队列已满返回fasle
            if (count >= capacity)
                return false;
            // 将node 作为链表尾结点
            Node<E> l = last;
            node.prev = l;
            last = node;
            if (first == null)
                first = node;
            else
                l.next = node;
            // 当前链表元素数量加1
            ++count;
            // 唤醒其他因为队列空而阻塞等待的线程(如果有)
            notEmpty.signal();
            // 入队成功返回true
            return true;
        }
    
    
    	// 队头入队
    	 private boolean linkFirst(Node<E> node) {
            // assert lock.isHeldByCurrentThread();
            if (count >= capacity)
                return false;
            // 队头入队
            Node<E> f = first;
            node.next = f;
            first = node;
            if (last == null)
                last = node;
            else
                f.prev = node;
            ++count;
            // 唤醒等待线程
            notEmpty.signal();
            return true;
        }
        
    

2.2 出队

同样,我们这里以 LinkedBlockingDeque#take 为例来看具体实现:

	// 默认队头元素出队
    public E take() throws InterruptedException {
        return takeFirst();
    }
	// 队头元素出队
    public E takeFirst() throws InterruptedException {
    	// 加锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E x;
            // 尝试出队,如果是空,则线程挂起等待
            while ( (x = unlinkFirst()) == null)
                notEmpty.await();
            // 出队成功返回出队元素
            return x;
        } finally {
            lock.unlock();
        }
    }

    public E takeLast() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E x;
            while ( (x = unlinkLast()) == null)
                notEmpty.await();
            return x;
        } finally {
            lock.unlock();
        }
    }

关键代码在 LinkedBlockingDeque#unlinkFirst 和 LinkedBlockingDeque#unlinkLast 中,其实现如下:


    /**
     * 删除并返回第一个元素,如果为空,则返回 null。
     */
    private E unlinkFirst() {
        // assert lock.isHeldByCurrentThread();
        Node<E> f = first;
        if (f == null)
            return null;
        // 队头元素出队
        Node<E> n = f.next;
        E item = f.item;
        f.item = null;
        f.next = f; // help GC
        first = n;
        if (n == null)
            last = null;
        else
            n.prev = null;
        // 队列元素减一
        --count;
        // 唤醒因为队满而等待入队的线程
        notFull.signal();
        return item;
    }


    /**
     * 删除并返回最后一个元素,如果为空,则返回 null。
     */
    private E unlinkLast() {
        // assert lock.isHeldByCurrentThread();
        Node<E> l = last;
        if (l == null)
            return null;
		// 队尾元素出队
        Node<E> p = l.prev;
        E item = l.item;
        l.item = null;
        l.prev = l; // help GC
        last = p;
        if (p == null)
            first = null;
        else
            p.next = null;
        --count;
        // 唤醒因为队满而等待入队的线程
        notFull.signal();
        return item;
    }

除此之外,还存在一个 LinkedBlockingDeque#unlink 方法,该方法是删除指定的元素,其实现如下:

    void unlink(Node<E> x) {
        // assert lock.isHeldByCurrentThread();
        Node<E> p = x.prev;
        Node<E> n = x.next;
        // p 为空认为 x 是队首元素,队首元素出队
        if (p == null) {
            unlinkFirst();
        } else if (n == null) {
        	// n 为空则认为是对尾元素,队尾出队
            unlinkLast();
        } else {
        	// 将 x 出队
            p.next = n;
            n.prev = p;
            x.item = null;
            // Don't mess with x's links.  They may still be in use by
            // an iterator.
            --count;
            // 唤醒因为队满而等待入队的线程
            notFull.signal();
        }
    }

在 LinkedBlockingDeque#removeFirstOccurrence 和 LinkedBlockingDeque#removeLastOccurrence 中都会调用,如下:

	// 从队列中移除 o 第一次出现的元素节点
    public boolean removeFirstOccurrence(Object o) {
        if (o == null) return false;
        // 加锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	// 从头开始遍历,直至 找到与o 相等的元素,移除后返回 true
            for (Node<E> p = first; p != null; p = p.next) {
                if (o.equals(p.item)) {
                    unlink(p);
                    return true;
                }
            }
            return false;
        } finally {
            lock.unlock();
        }
    }
	// 从队列中移除 o 最后一次出现的元素节点
    public boolean removeLastOccurrence(Object o) {
        if (o == null) return false;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	// 从尾开始遍历,直至 找到与o 相等的元素,移除后返回 true
            for (Node<E> p = last; p != null; p = p.prev) {
                if (o.equals(p.item)) {
                    unlink(p);
                    return true;
                }
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

3. DelayQueue

DelayQueue 是延迟无界阻塞队列,我们先来看下简单的使用实例:

@Slf4j
public class DelayQueueMain {
    public static void main(String[] args) throws InterruptedException {
        DelayQueue<DelayedItem> delayQueue = new DelayQueue<>();
        delayQueue.add(new DelayedItem("1", 1000L));
        delayQueue.add(new DelayedItem("2", 2000L));
        delayQueue.add(new DelayedItem("3", 3000L));
        delayQueue.add(new DelayedItem("4", 4000L));

        log.info("start");
        while (true) {
            final DelayedItem take = delayQueue.take();
            log.info("take.getMessage() = " + take.getMessage());
        }

    }

    @Getter
    public static class DelayedItem implements Delayed {
        private String message;

        private long delayTime;

        public DelayedItem(String message, long delayTime) {
            this.message = message;
            this.delayTime = delayTime + System.currentTimeMillis();
        }

        /**
         * 获取延迟时间,距离当前时间延迟多久后执行,当返回值小于0 时才会开始执行
         *
         * @param unit
         * @return 剩余的延迟;零或负值表示延迟已经过去
         */
        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

        /**
         * 优先级,会根据返回值判断在队列中的先后顺序
         *
         * @param o
         * @return
         */
        @Override
        public int compareTo(Delayed o) {
            return (int) (this.delayTime - ((DelayedItem) o).delayTime);
        }
    }
}

输出如下:
Java基础 : BlockingQueue浅析_第3张图片
可以看到,输出是按照延迟时间输出的。不过需要注意的是 Delayed#compareTo 方法决定了队列元素执行的优先级顺序,谁的优先级高谁作为对头元素,Delayed#getDelay 决定延迟执行时间,这两个方法并没有强关联。DelayQueue的实现是队头元素未出队执行前,其他元素即使到达延迟时间也不会执行。如下例子,因为 new DelayedItem("10", 10000L) 的优先级最高,所以其作为表头,即是其他 DelayedItem 延迟时间已经到了,仍需要等待表头的元素执行结束才能执行。

@Slf4j
public class DelayQueueMain {
    public static void main(String[] args) throws InterruptedException {
        DelayQueue<DelayedItem> delayQueue = new DelayQueue<>();
        delayQueue.add(new DelayedItem("1", 1000L));
        delayQueue.add(new DelayedItem("2", 2000L));
        delayQueue.add(new DelayedItem("3", 3000L));
        delayQueue.add(new DelayedItem("4", 4000L));

        delayQueue.add(new DelayedItem("10", 10000L) {
            @Override
            public int compareTo(Delayed o) {
                return -1;
            }
        });

        log.info("start");
        while (true) {
            final DelayedItem take = delayQueue.take();
            log.info("take.getMessage() = " + take.getMessage());
        }

    }

    @Getter
    public static class DelayedItem implements Delayed {
        private String message;

        private long delayTime;

        public DelayedItem(String message, long delayTime) {
            this.message = message;
            this.delayTime = delayTime + System.currentTimeMillis();
        }

        /**
         * 获取延迟时间,距离当前时间延迟多久后执行,当返回值小于0 时才会开始执行
         *
         * @param unit
         * @return 剩余的延迟;零或负值表示延迟已经过去
         */
        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

        /**
         * 优先级,会根据返回值判断在队列中的先后顺序
         *
         * @param o
         * @return
         */
        @Override
        public int compareTo(Delayed o) {
            return (int) (this.delayTime - ((DelayedItem) o).delayTime);
        }
    }
}

Java基础 : BlockingQueue浅析_第4张图片


简单总结下 DelayQueue 的实现逻辑:在使用 DelayQueue 队列时需要注意,DelayQueue 有两个方法需要实现:

  • DelayQueue#compareTo :返回值决定不同任务的执行顺序。
  • DelayQueue#getDelay :返回值决定延迟多久后执行。

需要注意的是,如果一个队列元素优先级在前,那么即是他后面的队列元素延迟时间已经到了,仍然需要等待前一个队列元素执行结束后才会执行。


下面来简单看下DelayQueue 的具体实现逻辑:

3.1 PriorityQueue

在 DelayQueue 中的有一个属性 q 初始化如下:

	// 未执行排序器,则使用元素自然排序
    private final PriorityQueue<E> q = new PriorityQueue<E>();

这说明 q 的实现是 PriorityQueue ,是一个优先级队列,其队列元素并非按照先进先出的逻辑,而是按照指定的优先级进行排序入队。下面我们来看其入队出队方法,其实现并不复杂,这里不再赘述。

  • 入队方法: PriorityQueue#offer

        public boolean offer(E e) {
            if (e == null)
                throw new NullPointerException();
            // 修改数量,安全失败使用,在迭代时如果发现 modCount 修改了则说明在迭代期间数组被修改,则抛出异常
            modCount++;
            int i = size;
            if (i >= queue.length)
            	// 需要的话扩容数组大小
                grow(i + 1);
            size = i + 1;
            if (i == 0)
            	// 如果当前添加的元素为第一个元素直接赋值
                queue[0] = e;
            else
            	// 按照优先级排序入队
                siftUp(i, e);
            return true;
        }
    
  • 出队方法 : PriorityQueue#poll

        public E poll() {
        	// 没有元素返回 null
            if (size == 0)
                return null;
            int s = --size;
            // 修改次数,安全失败使用,在迭代时如果发现 modCount 修改了则说明在迭代期间数组被修改,则抛出异常
            modCount++;
            // 数组第一个元素出队,因为在入队时已经按照优先级进行了排序
            E result = (E) queue[0];
            E x = (E) queue[s];
            queue[s] = null;
            if (s != 0)
                siftDown(0, x);
            return result;
        }
    

3.2 关键方法

  1. 入队方法 :DelayQueue#offer

        public boolean offer(E e) {
            final ReentrantLock lock = this.lock;
            // 线程加锁
            lock.lock();
            try {
            	// 这里的 q  是 PriorityQueue,PriorityQueue 入队元素保证优先级
                q.offer(e);
                // 获取队列头元素,如果等于e 则说明e入队后作为队头
                // 则需要将 leader 重置,同时唤醒等待线程重新去竞争
                if (q.peek() == e) {
                	// 重置 leader 
                    leader = null;
                    // 唤醒所有等待线程
                    available.signal();
                }
                return true;
            } finally {
            	// 释放锁
                lock.unlock();
            }
        }
    
  2. 出队方法:DelayQueue#poll

        public E poll() {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
            	// 获取队头元素
                E first = q.peek();
                // 队头为空 或者 队头元素延迟时间未到,返回 null
                if (first == null || first.getDelay(NANOSECONDS) > 0)
                    return null;
                else
                    return q.poll();
            } finally {
                lock.unlock();
            }
        }
    
  3. 出队方法 :DelayQueue#take

        public E take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                for (;;) {
                	// 获取队头元素
                    E first = q.peek();
                    // 队头元素为空则阻塞等待
                    if (first == null)
                        available.await();
                    else {
                    	// 获取队头元素延迟时间
                        long delay = first.getDelay(NANOSECONDS);
                        // 小于0则说明延迟时间已经到了,可以出队了
                        if (delay <= 0)
                            return q.poll();
                        // 到这里说明队头不为空并且延迟时间未到
                        first = null; // don't retain ref while waiting
                        // leader != null 说明有其他线程已经竞争等待该队头元素,则当前线程无限等待
                        // 直至当前队头元素被处理或者有新元素入队并且成为了队头
                        if (leader != null)
                            available.await();
                        else {
                        	// leader  赋值,声明当前线程获取到了队头元素的等待权
                            Thread thisThread = Thread.currentThread();
                            leader = thisThread;
                            try {
                            	// 等待延迟队列剩余的延迟时间
                                available.awaitNanos(delay);
                            } finally {
                            	// 如果 leader = thisThread 则说明等待过程中没有元素入栈,则重置 leader 
                                if (leader == thisThread)
                                    leader = null;
                            }
                        }
                    }
                }
            } finally {
            	// leader 为空说明当前没有线程在等待队头元素 && 队列元素不为空
                if (leader == null && q.peek() != null)
                	// 唤醒其他等待的线程进行新一轮的竞争
                    available.signal();
                // 释放锁
                lock.unlock();
            }
        }
    

以上:内容部分参考
https://blog.csdn.net/xiaoguangtouqiang/article/details/124109337
https://blog.csdn.net/zlfing/article/details/109802531
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

你可能感兴趣的:(Java,java)