阻塞队列ArrayBlockingQueue源码理解(JDK 1.8)

阻塞队列ArrayBlockingQueue源码理解(JDK 1.8)

      • 阻塞队列简介
      • 类继承结构
      • 入队列操作
      • 出队列操作
      • 分析总结

阻塞队列简介

阻塞队列顾名思义是一个队列的数据结构,ArrayBlockingQueue采用数组来实现这个队列,与其对应的LinkedBlockingQueue则采用链表来实现的。那么既然是一个队列,那么就会有入队列和出队列两种操作。

那阻塞队列中的阻塞代表的是什么意思呢?我认为这里的阻塞只会发生在队列的容量有限制的情况,如果没有容量限制的话就谈不上阻塞,但是这样可能会发生内存溢出问题。
假如每个队列应该都有容量限制,当队列中存储的元素达到上限时,再执行入队操作时,那么当前线程就会被阻塞直到有剩余空间能够存储时才唤醒继续尝试存储。当队列中已经没有元素可以出队列时,该线程也会被阻塞直到有元素在队列中继续尝试将元素出队列。

类继承结构

来看看ArrayBlockingQueue的类继承结构
阻塞队列ArrayBlockingQueue源码理解(JDK 1.8)_第1张图片
这里值得一提的是,入队操作的方法有很多个,在Queue接口中声明的入队和出队的方法在ArrayBlockingQueue中均没有实现具备阻塞的功能,所以要想有上面说的阻塞功能则应该调用BlockingQueue接口中自己声明的入队和出队列的方法。

下面就分析BlockingQueue接口中的put(E e)take()入队和出队方法的实现,揭开其阻塞原理的神秘面纱,其实就是使用了ReentrantLock锁。

入队列操作

首先看一下类的一些成员变量

/** The queued items */
/** 元素进入队列后存放的地方 */
final Object[] items;

/** items index for next take, poll, peek or remove */
/** 下一次出队列的元素在数组中的下标 */
int takeIndex;

/** items index for next put, offer, or add */
/** 下一次进入队列的元素存储在数组中的位置下标 */
int putIndex;

/** Number of elements in the queue */
/** 队列中存放的元素个数 */
int count;

/** Main lock guarding all access */
/** ReentrantLock锁,用于并发控制,保证线程安全 */
final ReentrantLock lock;

/** Condition for waiting takes */
/** 等待出队列的锁条件 */
private final Condition notEmpty;

/** Condition for waiting puts */
/** 等待入队列的锁条件 */
private final Condition notFull;

入队列方法put(E e)

/**
 * 插入元素到队尾,如果队列已满则阻塞等待有空间再进行入队操作
 * 
 * @throws InterruptedException {@inheritDoc}
 * @throws NullPointerException {@inheritDoc}
 */
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();
    }
}

/**
 * 在记录的下一次插入元素的位置插入元素,并且发出队列不再是空的信号。
 * 这个方法只会被获得了锁的线程调用
 * Inserts element at current put position, advances, and signals.
 * Call only when holding lock.
 */
private void enqueue(E x) {
	//这句断言也证明了上面说的‘这时只会有一个线程会到达这里’
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    //将元素放入数组
    items[putIndex] = x;
    //增加记录下一次应插入的下标
    //如果到达数组尾边界,则赋值为0,从数组头部开始
    if (++putIndex == items.length)
        putIndex = 0;
    //增加队列中的元素总数
    count++;
    //发出队列不为空信号,可以唤醒第一个阻塞在出队列方法上的线程
    notEmpty.signal();
}

出队列操作

出队列所作的操作和入队列大致相同

/**
 * 接受并移除队列中的首个元素,如果队列为空的
 * 那么会阻塞直到不会空时再进行出队列操作
 * Retrieves and removes the head of this queue, waiting if necessary
 * until an element becomes available.
 *
 * @return the head of this queue
 * @throws InterruptedException if interrupted while waiting
 */
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    //获取锁
    lock.lockInterruptibly();
    try {
    	//如果容量为0,那么阻塞当前线程
        while (count == 0)
            notEmpty.await();
        //首元素出对列操作
        return dequeue();
    } finally {
        lock.unlock();
    }
}

/**
 * 指定位置的元素出队列,并且发出队列有剩余空间的信号
 * 该方法只会被持有锁的线程调用
 * Extracts element at current take position, advances, and signals.
 * Call only when holding lock.
 */
private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    //获取takeIndex位置处的元素
    E x = (E) items[takeIndex];
    //将原位置置为null
    items[takeIndex] = null;
    //将takeIndex下一次出队列位置加1,如果到达数组边界,那么从头开始
    if (++takeIndex == items.length)
        takeIndex = 0;
    //元素总数减1
    count--;
    //这里的意思应该是有元素出队列,那么要同步迭代器。TODO 还有待研究
    if (itrs != null)
        itrs.elementDequeued();
    //发出队列有剩余空间的信号,可以唤醒第一个阻塞在入队列方法上的线程
    notFull.signal();
    return x;
}

分析总结

1、 ArrayBlockingQueue采用数组来存放数据,定义了两个整数变量takeIndexputIndex 分别指向下一次需要出队列的元素,和下一次有元素入队列的位置。这样的好处是当入出队列时,可以不用移动数组中元素的位置降低开销。

2、 阻塞队列的关键就在于怎么阻塞,使用到了AQS抽象队列同步器(AbstractQueuedSynchronizer)中的条件阻塞对象ConditionObject。其实可以简单的理解为调用该对象的await()方法阻塞当前线程,调用signal()方法唤醒阻塞队列中的第一个阻塞的线程。
我认为也可以类似地用synchronized来实现上面入队列的相同过程,这样也可以方便更好的理解其中锁条件对象 notFullnotEmpty 的作用。

private Object lock = new Object();
private Object notFullObject = new Object();
public void put(E e) throws InterruptedException {
	synchronized(lock) {
		while (count == items.length) {
			synchronized(notFullObject ) {
				//阻塞当前线程,并且会释放获取的notFull锁
				notFullObject .wait();
			}
		}
		//入队列操作
		enqueue(e);
	}
}

这样在有元素出队列后可以调用notFull.notify();随机唤醒其中一个阻塞在入队列过程中的线程继续执行入队列操作。
当然这种方式并没有ConditionObject好,这样只是有助于理解ConditionObject。

以上是本人的理解,若有错误,欢迎指正!

你可能感兴趣的:(Java)