主要内容
ArrayBlockingQueue.
这篇博客主要说说ArrayBlockingQueue这个阻塞队列的存储结构以及针对多线程的情况,这个阻塞队列是怎样实现多线程安全的,还有就是一些方法的区别。
其实对于多线程我也是慢慢体会到的,看了源码,看了别人的实现方式,才渐渐懂得怎样处理多线程安全的问题。
ArrayBlocking采用的是数组的存储方式。
希望你们也是如此。
当然在说的过程,采用源码来说是比较有权威的。
ArrayBlockingQueue
ArrayBlockingQueue是采用的是循环数组的形式表达队列,所以在该类中不存在扩容的情况,对于数组的长度来说,根据初始化的参数为标准,类中没有默认的数组长度。
其实对于这个类是怎样来实现多线程安全的呢,从这个类的成员就可以猜想到。
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
final Object[] items;//存放元素的数组
int takeIndex;//记录元素被取出元素的数组下标
int putIndex;//记录元素被放入元素的数组下标
int count;//记录元素的个数
final ReentrantLock lock;//锁,同样也是保证多线程安全的一个重要因素
private final Condition notEmpty;
//notEmpty是当前lock的阻塞队列
//作用就是采用内部的一个Condition队列来存储想通过put进行添加元素,但由于数组已满而被阻塞的线程。
private final Condition notFull;
//notFull是当前lock的另一个阻塞队列
//作用就是采用内部的Condition队列来存储想通过take进行取出元素,但由于数组为空而被阻塞的线程。
transient Itrs itrs = null;
}
刚才说,可以通过成员来推测出这个类采用的什么手段来实现多线程。其实我是看完之后才进行总结的,不是我一开始看的时候就知道。
那我们一步一步来看,
这个类给我们提供了很多添加和取出的方法,而多线程安全的处理当然是在方法上进行处理的。
那为什么说通过成员就可以看出来实现手段呢,
int count 这个成员是用来记录数组中当前拥有的元素的个数,那么通过添加和取出都要对这个变量进行操作,而这个成员并没有采用volitile进行修饰。但是该类采用了一个ReentrantLock lock,那我们就可以得出结论,这个类在所有的方法上都采用lock进行多线程安全的保证。所以对于count来说可以不添加任何有关多线程安全的修饰。
为什么这么说呢,那首先就要对ReentrantLock 有一定的认识,如果不了解请看博客ReentrantLock.相信你就会理解。
因为我们知道一个ReentrantLock 对象对应一个AQS,所以对于ArrayBlockingQueue来说,如果你对一个ArrayBlockingQueue对象进行操作(加入或者取出),那当然对于这个对象来说也只有一个ReentrantLock 对象作为其ArrayBlockingQueue类的成员。
同样是因为在ArrayBlockingQueue类中的加入和取出的方法中都要对其lock进行获取,而lock对于一个ArrayBlockingQueue对象来说只有一个,那么在多线程进行调用加入或者取出的方法的时候,自然会排队进行,因为对于多线程来说,他们认识的是同一把锁lock,当有线程在执行加入或者取出的方法的时候,自然会持有这把锁,那么对于其他线程来说就只能加入到lock锁的等待队列中,等一个线程执行完,释放锁唤醒等待队列中的等待线程的时候,那些等待线程才会去挨个执行他们的操作。
所以对于ArrayBlockingQueue的成员count 没有采用任何的修饰,只是一个普通的变量,因为对于每个线程对ArrayBlockingQueue 对象所执行的操作都是顺序的,所以count具有准确性。
那我们来看ArrayBlockingQueue这个类中给我们提供的操作方法。
不是白看的,你们找这些个方法的一个共同点。你们找完之后,我会慢慢说这些个方法的具体情况。
poll();
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();//这里的lock就是ArrayBlockingQueue类中唯一的锁lock。
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
offer(E e)
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
put(E e)
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
take()
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
找出来了吗?
通过上边的方法,我们也能清楚的看到,每个方法开头都采用了lock.lock,方法的结尾都采用了lock.unlock。
所以可以说对于ArrayBlockingQueue来说,实现多线程的手段比较粗鲁,就整了一个ReentrantLock 对象作为成员,就可以让多线程在方法的执行上做到同步了,同样也就实现类多线程安全。
所以ArrayBlockingQueue类中还有其他成员
final Object[] items;
int takeIndex;
int putIndex;
例如这三个成员,也都没有任何的关于线程安全的修饰,因为一个lock就做到了。
下面我们来说说offer()和put()的区别,take() 和poll() 的区别
put() 和 offer()
说到offer你们可以回看上面的方法,对于offer来说,当一个线程调用offer的时候,如果此时数组已经满员了,那么offer会直接返回false。表示不能够添加
而对于put()来说,同样也是添加,但是就不一样了,当一个线程调用put()的时候,如果此时数组已经满员了,那么你回看上边的方法,你会看到 notFull.await();这样的代码,此时对于调用put方法的这个线程来说,会进行阻塞,而不是像offer一样直接返回false。这个nofull成员我在一开始的时候已经说过了。
其实在这个notfull所带领的阻塞队列中的线程表明都是等待添加的线程,如果你看过我写的博客Condition.你就会知道Condition阻塞队列的原理。然后就等待被唤醒了。
take() 和 poll()。
poll方法可以回看上边的源码,表示取出一个元素,当数组为空时,则直接返回null。
而take方法,倒是和put方法有的一拼,当数组为空时,会执行notEmpty.await();这个代码,表明将调用take方法的线程进行阻塞。但对于区操作,是将操作的这个线程阻塞起来,加入到notEmpty带领的这个阻塞队列中。然后就等待被唤醒了。
其实不管是notEmpty和是notFull,这两个Condition对象都是由lock这一个对象生成的,说明不管是在两个阻塞队列中任何一个阻塞线程,在被唤醒之后都是要进入到lock的等待队列中的。因为不管在什么时候,只有可能是一个线程持有锁。
既然说到了阻塞线程需要被唤醒,何时被唤醒?
对于put来说,表明数组中没有位置,所以无法put,只能阻塞,当一旦有poll操作的时候,poll方法会调用一个dequeue方法这个方法就是将数组中最老的元素进行取出,关键是在最后有一句 notFull.signal();
采用这句进行唤醒在notFull阻塞队列中的线程,被唤醒的线程加入到lock的等待队列中,等待机会执行。
private E dequeue() {
final Object[] items = this.items;
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
对于take来说,数组为空时,无法获取,进行阻塞,当一旦有offer操作的时候,offer操作会调用enqueue()方法,进行元素的添加,在最后进行notEmpty.signal();唤醒等待取操作的线程,被唤醒的线程加入到lock的等待队列中,等待机会执行。
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
总结:
对于ArrayBlockingQueue类来说,采用实现多线程安全的手法还是比较简单,但同时也是我们可能最常用的一种手段,最主要的还是ArrayBlockingQueue应用了Reentrantlock这个类来进行实现的,还有Condition与Reentrantlock的结合。ArrayBlockingQueue中对元素的添加和删除的本质对循环数组的操作我们都很熟悉,最主要的还是能够理解Reentrantlock和Condition这两个类的原理,这样再回头自己看这个应用这两个类的阻塞队列的实现就会变得简单很多。