从ReentrantLock开始学习AQS-2(BlockingQueue)

76 阅读4分钟

BlockingQueue,是java.util.concurrent 包提供的用于解决并发生产者 - 消费者问题的最有用的类,它的特性是在任意时刻只有一个线程可以进行take或者put操作,并且BlockingQueue提供了超时return null的机制,在许多生产场景里都可以看到这个工具的身影。

队列类型

  1. 无限队列 (unbounded queue ) - 几乎可以无限增长
  2. 有限队列 ( bounded queue ) - 定义了最大容量

**队列数据结构 **
队列实质就是一种存储数据的结构

  • 通常用链表或者数组实现
  • 一般而言队列具备FIFO先进先出的特性,当然也有双端队列(Deque)优先级队列
  • 主要操作:入队(EnQueue)与出队(Dequeue)

队列.png 常见的4种阻塞队列

  • ArrayBlockingQueue 由数组支持的有界队列
  • LinkedBlockingQueue 由链接节点支持的可选有界队列
  • PriorityBlockingQueue 由优先级堆支持的无界优先级队列
  • DelayQueue 由优先级堆支持的、基于时间的调度队列

ArrayBlockingQueue

队列基于数组实现,容量大小在创建ArrayBlockingQueue对象时已定义好

数据结构如下图:

arrayBlockingQueue.png BlockingQueue blockingQueue = new ArrayBlockingQueue<>();

应用场景

在线程池中有比较多的应用,生产者消费者场景

工作原理

基于ReentrantLock保证线程安全,根据Condition实现队列满时的阻塞

LinkedBlockingQueue

是一个基于链表的无界队列(理论上有界)

BlockingQueue blockingQueue = new LinkedBlockingQueue<>();

上面这段代码中,blockingQueue 的容量将设置为 Integer.MAX_VALUE 。

向无限队列添加元素的所有操作都将永远不会阻塞,[注意这里不是说不会加锁保证线程安全],因此它可以增长到非常大的容量。

使用无限 BlockingQueue 设计生产者 - 消费者模型时最重要的是 消费者应该能够像生产者向队列添加消息一样快地消费消息 。否则,内存可能会填满,然后就会得到一个 OutOfMemory 异常。

DelayQueue

由优先级堆支持的、基于时间的调度队列,内部基于无界队列PriorityQueue实现,而无界队列基于数组的扩容实现。

队列创建: BlockingQueue blockingQueue = new DelayQueue();

要求: 入队的对象必须要实现Delayed接口,而Delayed集成自Comparable接口

工作原理:队列内部会根据时间优先级进行排序。延迟类线程池周期执行。

BlockingQueue API

BlockingQueue 接口的所有方法可以分为两大类:负责向队列添加元素的方法和检索这些元素的方法。在队列满/空的情况下,来自这两个组的每个方法的行为都不同。

添加元素

方法说明
add()如果插入成功则返回 true,否则抛出 IllegalStateException 异常
put()将指定的元素插入队列,如果队列满了,那么会阻塞直到有空间插入
offer()如果插入成功则返回 true,否则返回 false
offer(E e, long timeout, TimeUnit unit)尝试将元素插入队列,如果队列已满,那么会阻塞直到有空间插入

检索元素

方法说明
take()获取队列的头部元素并将其删除,如果队列为空,则阻塞并等待元素变为可用
poll(long timeout, TimeUnit unit)检索并删除队列的头部,如有必要,等待指定的等待时间以使元素可用,如果超时,则返回 null

在构建生产者 - 消费者程序时,这些方法是 BlockingQueue 接口中最重要的构建块。

ArrayBlockingQueue

创建

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    //设置item的长度
    this.items = new Object[capacity];
    //创建锁
    lock = new ReentrantLock(fair);
    //有界阻塞队列 用来表示"非空"条件,当队列为空时,
    //阻塞消费者线程,当队列元素数量大于0时,唤醒等待着的消费者线程,
    //保证消费者线程在有元素的情况下能够顺利取出队列中的元素。
    //notEmpty条件变量一般与take操作相关,即take操作在遇到队列为空时,
    //会调用notEmpty.await()方法进入等待状态,直到队列中有元素后,
    //notEmpty.signal()方法被调用唤醒线程  不为空信号被唤醒 就可以添加元素。
    notEmpty = lock.newCondition();
    //表示“队列非满”条件,当队列已满时,阻塞生产者线程,
    //当队列元素数量小于数组长度时,唤醒等待中的生产者线程,
    //保证生产者线程在队列非满的情况下能够往队列中添加元素。
    //notFull条件变量一般与put操作相关,即put操作在遇到队列已满时
    //调用notFull.await()方法进入等待状态,
    //直到队列中有空位后,notFull.signal()方法被调用唤醒线程  没有满信号被唤醒就可以获取元素。
    notFull =  lock.newCondition();
}

put

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    //加锁
    lock.lockInterruptibly();
    try {
        while (count == items.length){
        //队列已满 notFull 等待
             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();
}

/**
 * 发新号,通知条件队列当中节点到同步队列当中去排队
 *
 */
public final void signal() {
    if (!isHeldExclusively())//节点不能已经持有独占锁
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    //判断条件等待队列first节点是不是空的 不是空的就取出来
    if (first != null)
        /**
         * 发信号通知条件队列的节点准备到同步队列当中去排队
         */
        doSignal(first);
}

/**
 * 发信号,通知遍历条件队列当中的节点转移到同步队列当中,准备排队获取锁
 */
private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) && //转移节点
            (first = firstWaiter) != null);
}

/**
 * 将节点从条件队列当中移动到同步队列当中,等待获取锁
 */
final boolean transferForSignal(Node node) {
    /*
     * 修改节点信号量状态为0,失败直接返回false
     */
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    /*
     * 加入同步队列尾部当中,返回前驱节点
     */
    Node p = enq(node);
    int ws = p.waitStatus;
    //前驱节点不可用 或者 修改信号量状态失败
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread); //唤醒当前节点
    return true;
}

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() {
    //首先获取队列头部的元素(但并不将其从队列中移除),并将其赋值给 `x` 变量
    final Object[] items = this.items;
    E x = (E) items[takeIndex];
    //将头部位置上的元素设置为 `null`。
    items[takeIndex] = null;
    //1.  将 `takeIndex` 增加 1,即指向下一个元素的位置,用来更新队列头部的位置。
    if (++takeIndex == items.length){
       takeIndex = 0;
    }
    count--;
    if (itrs != null)
        itrs.elementDequeued();
     //1.  如果当前队列是“满队列”,即 `count == items.length`,
     //则将 `notFull` 条件队列中的线程唤醒,因为有空位置可以添加元素了。
    notFull.signal();
    //返回 `x` 变量,即取出的队列头部元素
    return x;
}

/**
 * 发新号,通知条件队列当中节点到同步队列当中去排队
 *
 */
public final void signal() {
    if (!isHeldExclusively())//节点不能已经持有独占锁
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        /**
         * 发信号通知条件队列的节点准备到同步队列当中去排队
         */
        doSignal(first);
}