java中的阻塞队列

泛型珊瑚
• 阅读 1388

前言

在去年的面试过程中,被面试官问道“阻塞队列”这个问题,因为当时并没有对此问题进行深入理解,只是按照自己的理解说明了该问题,最后面试结果也不太好,今天对该问题进行简要的面试并记录如下;如有错误,欢迎指正。

什么是阻塞队列

在数据结构中,队列遵循FIFO(先进先出)原则。在java中,Queue接口定义了定义了基本行为,由子类完成实现,常见的队列有ArrayDequeLinkedList等,这些都是非线程安全的,在java 1.5中新增了阻塞队列,当队列满时,添加元素的线程呈阻塞状态;当队列为空时,获取元素的线程呈阻塞状态。

生产者、消费者模型

java中的阻塞队列

生产者将元素添加到队列中,消费中获取数据后完成数据处理。两者通过队列解决了生产者和消费者的耦合关系;当生产者的生产速度与消费者的消费速度不一致时,可以通过大道缓冲的目的。

阻塞队列的使用场景

  1. 线程池

    在线程池中,当工作线程数大于等于corePoolSize时,后续的任务后添加到阻塞队列中;

目前有那些阻塞队列

在java中,BlockingQueue接口定义了阻塞队列的行为,常用子类是ArrayBlockingQueueLinkedBlockingQueue

java中的阻塞队列

BlockingQueue继承了Queue接口,拥有其全部特性。在BlockingQueuejava doc中对其中的操作方法做了汇总

java中的阻塞队列

  • 插入元素

    • add(e):当队列已满时,再添加元素会抛出异常IllegalStateException
    • offer(e):添加成功,返回true,否则返回false
    • put:(e):当队列已满时,再添加元素会使线程变为阻塞状态
    • offer(e, time,unit):当队列已满时,在末尾添加数据,如果在指定时间内没有添加成功,返回false,反之是true
  • 删除元素:

    • remove(e):返回true表示已成功删除,否则返回false
    • poll():如果队列为空返回null,否则返回队列中的第一个元素
    • take():获取队列中的第一个元素,如果队列为空,获取元素的线程变为阻塞状态
    • poll(time, unit):当队列为空时,线程被阻塞,如果超过指定时间,线程退出
  • 检查元素:

    • element():获取队头元素,如果元素为null,抛出NoSuchElementException
    • peek():获取队头元素,如果队列为空返回null,否则返回目标元素

ArrayBlockingQueue

底层基于数组的有界阻塞队列,在构造此队列时必须指定容量;

构造函数

// 第一个    
public ArrayBlockingQueue(int capacity, boolean fair,Collection<? extends E> c) {
        this(capacity, fair);

        final ReentrantLock lock = this.lock;
        lock.lock(); // Lock only for visibility, not mutual exclusion
        try {
            int i = 0;
            try {
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }

    // 第二个
    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();
    }

    // 第三个
    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }
  • capacity:队列的初始容量
  • fair:线程访问队列的公平性。如果为true按照FIFO的原则处理,反之;默认为false
  • c:已有元素的集合,类型于合并两个数组

put()方法

   public void put(E e) throws InterruptedException {
         // 检查元素是否为null
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        // 获取锁
        lock.lockInterruptibly();
        try {
            // 如果当前队列为空,变为阻塞状态
            while (count == items.length)
                notFull.await();
            // 反之,就添加元素
            enqueue(e);
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    private void enqueue(E x) {
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        // 此时队列不为空,唤醒消费者
        notEmpty.signal();
    }

take()方法

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        // 获取锁
        lock.lockInterruptibly();
        try {
            // 如果队列为空,消费者变为阻塞状态
            while (count == 0)
                notEmpty.await();
            // 不为空,就获取数据
            return dequeue();
        } finally {
            // 解锁
            lock.unlock();
        }
    }

        private E dequeue() {
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        // 获取队头元素x
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
         // 此时队列没有满,同时生产者继续添加数据
        notFull.signal();
        return x;
    }

LinkedBlockingQueue

底层基于单向链表的无界阻塞队列,如果不指定初始容量,默认为Integer.MAX_VALUE,否则为指定容量

构造函数

    // 不指定容量     
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
    // 指定容量
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

    // 等同于合并数组
    public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); // Never contended, but necessary for visibility
        try {
            int n = 0;
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                enqueue(new Node<E>(e));
                ++n;
            }
            count.set(n);
        } finally {
            putLock.unlock();
        }
    }

put()方法

    public void put(E e) throws InterruptedException {
        // 元素为空,抛出异常
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        // 获取队列中的数据量
        final AtomicInteger count = this.count;
        // 获取锁
        putLock.lockInterruptibly();
        try {
            // 队列满了,变为阻塞状态
            while (count.get() == capacity) {
                notFull.await();
            }
            // 将目标元素添加到链表的尾端
            enqueue(node);
            // 总数增加
            c = count.getAndIncrement();
            // 队列还没有满,继续添加元素
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            // 解锁
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

take()方法

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        // 获取队列中的工作数
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        // 获取锁
        takeLock.lockInterruptibly();
        try {
            // 如果队列为空,变为阻塞状态
            while (count.get() == 0) {
                notEmpty.await();
            }
            // 获取队头元素
            x = dequeue();
            // 递减
            c = count.getAndDecrement();
            // 通知消费者
            if (c > 1)
                notEmpty.signal();
        } finally {
            // 解锁
            takeLock.unlock();
        }
        if (c == capacity)
            // 
            signalNotFull();
        return x;
    }

对比

相同点

  1. 两者都是通过Condition通知生产者和消费者完成元素的添加和获取
  2. 都可以指定容量

不同点

  1. ArrayBlockingQueue基于数据,LinkedBlockingQueue基于链表
  2. ArrayBlockingQueue内有一把锁,LinkedBlockingQueue内有两把锁
    java中的阻塞队列
    java中的阻塞队列

自己动手实现一个阻塞队列

通过分析源码可以知道,阻塞队列其实是通过通知机制Condition完成生产者和消费的互通。也可以通过Object类中的wait()notifynotifyAll实现。下面是自己写的一个阻塞队列

public class BlockQueue {
    // 对象锁
    public static final Object LOCK = new Object();
    // 控制变量的值 来通知双方
    public boolean condition;
    
    public void put() {
        synchronized (LOCK) {
            while (condition) {
                try {
                    // 满了
                    System.out.println("put   队列满了,开始阻塞");
                    LOCK.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            condition = true;
            System.out.println("put   改为true,唤醒消费者");
            LOCK.notifyAll();
        }
    }


    public void take() {
        synchronized (LOCK) {
            while (!condition) {
                // 没满
                System.out.println("take   队列没满,开始阻塞");
                try {
                    LOCK.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            condition = false;
            System.out.println("take   改为false,唤醒生产者");
            LOCK.notifyAll();
        }
    }
}

参考文章:

并发容器之BlockingQueue (juejin.cn)

BlockingQueue (Java Platform SE 8 ) (oracle.com)


阅读原文

点赞
收藏
评论区
推荐文章
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
3年前
java多线程——CAS
关于无锁队列,网上有很多介绍了,我做一个梳理,从它是什么再到它有哪些特性以及应用做一个总结,方便自己使用和记录。本文主要内容:非阻塞同步是什么cas是什么特性ABA问题无阻塞队列1非阻塞同步互斥同步属于一种悲观的并发策略,总认为只要不去做正确的同步措施,肯定会出问题,无论共享数据是否真的会出现竞争,它都要进行加锁。而基
Dax Dax
4年前
如何使用vue中的nextTick
其实这个问题主要就是针对Vue的异步更新队列的理解,因为我们平时用的也比较少,所以很多时候都会忽略掉,但是如果我们在面试当中能比较详细的解答这个问题,那么我相信这应该会是一个闪光点,那话不多说,我们先来捋一下答题思路:答题思路:nextTick是什么?先来一个定义为什么需要他呢?异步更新队列实现原理解释什么地方使用到他呢?描述使用的场景如何使用他呢?描述使用
Stella981 Stella981
3年前
Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点?
面试题1.为什么使用消息队列?2.消息队列有什么优点和缺点?3.Kafka、ActiveMQ、RabbitMQ、RocketMQ都有什么区别,以及适合哪些场景?面试官心理分析其实面试官主要是想看看:第一,你知不知道你们系统里为什么要用消息队列这个东西?不少候选人,说自己项目里用了Redis、MQ,但是其实他并
Wesley13 Wesley13
3年前
MQ消息中间件,面试能问些什么?
MQ消息中间件,面试能问些什么?为什么使用消息队列?消息队列的优点和缺点?kafka、activemq、rabbitmq、rocketmq都有什么优缺点?面试官角度分析:(1)你知不知道你们系统里为什么要用消息队列这个东西?(2)既然用了消息队列这个东西,你知不知道用了有什么好处?(3
Wesley13 Wesley13
3年前
04.JUC 集合
基本概念LinkedBlockingQueue是一个用链表实现的有界阻塞队列。LinkedBlockingQueue按照先进先出的原则对元素进行排序。LinkedBlockingQueue采用了双锁、双条件队列来提高读写效率。内部构造LinkedBlockingQueue内部维
Wesley13 Wesley13
3年前
Java多线程之线程安全队列Queue
在Java多线程应用中,队列的使用率很高,多数生产消费模型的首选数据结构就是队列。Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。注:什么叫线程安全?这个首先要明确。
Wesley13 Wesley13
3年前
JAVA线程15
一、阻塞队列1\.概述阻塞队列是Java5线程新特征中的内容,Java定义了阻塞队列的接口java.util.concurrent.BlockingQueue。阻塞队列是一个指定长度的队列,如果队列满了,添加新元素的操作会被阻塞等待,直到有空位为止。同样,当队列为空时候,请求队列元素的操作同样会阻塞等待,直到有可用元
Stella981 Stella981
3年前
Netty学习笔记1:5种IO模型
1阻塞IO模型从字面来理解,就是调用时可能被阻塞,什么叫阻塞,要知道一个进程有N种状态,学过OS都知道如果阻塞,就会把当前进程放在某个条件的阻塞队列里。直到条件满足了,才会转移此进程进入就绪队列。当然,就绪队列还有个优先级的概念,就不扯远了。阻塞IO.1)调用API,比如 r
Wesley13 Wesley13
3年前
Java并发 阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加操作支持阻塞地插入和移除方法。支持阻塞插入的方法是指当队列满时会阻塞插入元素的线程,直到队列不满;支持阻塞移除的方法是指当队列为空时获取元素的线程无法继续获取元素直到队列不空。可以发现阻塞队列非常适合消费者和生产者场景下进行使用,生产者生产数据就是向阻塞队列中插入元素,消费者消
泛型珊瑚
泛型珊瑚
Lv1
总有人笨拙得想把所有温柔给你。
文章
3
粉丝
0
获赞
0