JDK源码阅读(十三) : 单端阻塞队列——ArrayBlockingQueue

1. 队列

队列是一种先进先出(First In First Out)的数据结构。

可以分成阻塞和非阻塞两类:

  • 阻塞队列,即如果队列已满,入队操作会阻塞;如果队列为空,出队操作也会阻塞。(Blocking标识)
  • 非阻塞队列,即如果队列已满,执行入队操作会直接返回;如果队列为空,执行出队操作也是直接返回。

也可以分成单端和双端两类:

  • 单端队列,即只能在队尾入队,队首出队;(Queue标识)
  • 双端队列,队首队尾都可以出队。(Deque标识)

单端阻塞队列:主要有ArrayBlockingQueue(内部是数组)、LinkedBlockingQueue(内部是链表)、PriorityBlockingQueue(优先级队列)等。

双端阻塞队列:主要有LinkedBlockingDeque(内部是链表)。

单端非阻塞队列:ConcurrentLinkedQueue。

双端非阻塞队列:ConcurrentLinkedDeque。


ArrayBlockingQueue和LinkedBlockingQueue是有界队列,工作中建议使用有界队列,防止数据量过大时出现OOM。

本文主要介绍ArrayBlockingQueue的代码实现。

2. ArrayBlockingQueue

2.1 继承关系

ArrayBlockingQueue继承了抽象类AbstractQueue,继承其中的一些方法。并且实现了BlockingQueue接口,实现了阻塞队列所需实现的一些方法。

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable 

2.2 属性

(1)Object数组,用于存储数据。 数组容量在构造实例时由用户确定,不可扩容。

final Object[] items;

(2)队头位置

int takeIndex;

(3)队尾位置

int putIndex;

(4)队列中的元素数

int count;

(5)可重入锁,即线程可以重复获取的锁。比如:当某一个线程获取了锁,然后执行临界代码,执行过程中又遇到这把锁,如果该锁是可重入锁,此时可以加锁成功;否则,就会阻塞。锁具有不可变性,所以用final修饰。

final ReentrantLock lock;

(6)条件变量notEmpty。当从队列中取元素时,如果不满足条件notEmpty,就不能成功获取。条件变量也是不可变的,所以用final修饰。

private final Condition notEmpty;

(7)条件变量notFull。当添加元素到队列中时,如果不满足条件notFull,就不能成功添加。

private final Condition notFull;

注意:Lock和Condition是java中管程的实现方式,管程实现原理可参考本人的另一篇博文:https://blog.csdn.net/Longstar_L/article/details/109489227

2.3 构造器

(1)设置队列长度的构造器,调用了(2)中的构造器,fair默认为false。

public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}

(2)设置队列长度,并且可以自定义fair的构造器。

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
  	//分配数组空间,长度为capacity
    this.items = new Object[capacity];
  	//创建可重入锁实例
    lock = new ReentrantLock(fair);
  	//创建2个条件变量
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

fair指的是队列中所有的可重入锁是公平锁还是非公平锁。

  • 公平锁:当有线程释放锁,会从阻塞队列中唤醒一个等待时间最久的线程。
  • 非公平锁:不提供公平保证,可能刚到来的线程会直接获取到锁。

2.4 入队

(1)**add(E e)**方法在队尾插入一个元素。如果队列已满,会直接抛出异常。属于非阻塞方法。

public boolean add(E e) {
  	//调用父类的add方法
    return super.add(e);
}
public boolean add(E e) {
    if (offer(e))
        return true;
    else
      	//如果队列已满,添加失败,就会直接抛出异常
        throw new IllegalStateException("Queue full");
}

最终调用的还是本类实现的offer方法。

public boolean offer(E e) {
  	//判断e是否是null
    checkNotNull(e);
  	//拿到可重入锁
    final ReentrantLock lock = this.lock;
  	//try...finally...的模式加锁和解锁
    lock.lock();
    try {
      	//如果队列已满,返回false
        if (count == items.length)
            return false;
        else {
          	//否则,入队
            enqueue(e);
            return true;
        }
    } finally {
        lock.unlock();
    }
}
private void enqueue(E x) {
    final Object[] items = this.items;
  	//队尾放上x
    items[putIndex] = x;
  	//队尾后移一位。采用的是环形队列,如果队尾超出了边界,就回到起始位置
    if (++putIndex == items.length)
        putIndex = 0;
  	//总元素数加1
    count++;
  	//notEmpty的阻塞队列中唤醒一个线程,重新回到管程入口等待入口锁
    notEmpty.signal();
}

(2)offer(E)方法在队列已满的情况下,直接返回false,不抛异常。属于非阻塞方法。

(3)put方法采用lock+condition的管程方式。属于阻塞方法

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
  	//可中断地加锁
    lock.lockInterruptibly();
    try {
      	//如果队列满了,就将当前线程放入条件变量notFull的阻塞队列,
      	//当有其他线程执行notFull.signal,就会从该阻塞队列中唤醒一个线程回到管程入口处再次尝试获取锁,
      	//当获取到锁后,会在await方法处返回,
      	//必须使用while循环重新验证该条件是否还满足
        while (count == items.length)
            notFull.await();
      	//重新验证队列不满,就可以执行入队操作
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

lock.lockInterruptibly()说明:如果一个线程获取了锁lock之后,在获取另一把锁时又阻塞了,现在我们给该阻塞线程发送中断信号,可以唤醒它,就有机会释放其已经占有的锁A。

(4)offer(E e, long timeout, TimeUnit unit)方法是管程+支持超时的入队方法。属于阻塞+超时方法。

public boolean offer(E e, long timeout, TimeUnit unit)
    throws InterruptedException {

    checkNotNull(e);
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
  	//可中断地加锁
    lock.lockInterruptibly();
    try {
        while (count == items.length) {
          	//如果超时了,直接返回false
            if (nanos <= 0)
                return false;
          	//将剩余等待时间传入,返回时会得到新的剩余时间
            nanos = notFull.awaitNanos(nanos);
        }
        enqueue(e);
        return true;
    } finally {
        lock.unlock();
    }
}

2.5 出队

(1)take方法,属于阻塞方法。

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
      	//队列为空时,在notEmpty的阻塞队列中等待
        while (count == 0)
            notEmpty.await();
      	//返回出队元素
        return dequeue();
    } finally {
        lock.unlock();
    }
}
private E dequeue() {
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
  	//获取队头元素
    E x = (E) items[takeIndex];
  	//清空该位置
    items[takeIndex] = null;
  	//队头后移一位,如果越界了,就回到起始位置
    if (++takeIndex == items.length)
        takeIndex = 0;
  	//元素数减1
    count--;
    if (itrs != null)
        itrs.elementDequeued();
  	//notFull的阻塞队列中唤醒一个线程
    notFull.signal();
  	//返回出队元素
    return x;
}

(2)poll方法在队列为空时,直接返回null,属于非阻塞方法。

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return (count == 0) ? null : dequeue();
    } finally {
        lock.unlock();
    }
}

2.6 查找队头元素

peek()方法。如果队列为空,直接返回null。

public E peek() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return itemAt(takeIndex); // null when queue is empty
    } finally {
        lock.unlock();
    }
}

2.7 获取队列的元素数

public int size() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return count;
    } finally {
        lock.unlock();
    }
}

2.8 小结

  • ArrayBlockingQueue总体上采用Lock+Condition的管程方式。
  • 所有的插入、删除、查找方式都使用同一把锁ReentrantLock。
  • 设置两个条件变量notFull和notEmpty,来同步线程间的入队、出队操作,防止队空时出队、队满时入队。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值