关键词
阻塞
有界
单锁双条件
final Object[] items;//底层数组
int takeIndex; //出队索引,由lock保证线程安全性
int putIndex;//入队索引,由lock保证线程安全性
int count;
/*
* Concurrency control uses the classic two-condition algorithm */
final ReentrantLock lock; //锁对象,AQS实现
private final Condition notEmpty;//Object wait和notify的替代品
private final Condition notFull;//Object wait和notify的替代品
构造器
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();
}
定义数组大小,所以有界。只有一把重入锁和这把锁上的两个condition,notEmpty控制队列为空时线程的阻塞和唤醒,notFull 控制队列满时线程的阻塞和唤醒。
入队
先看enqueue方法,他在offer,put方法用来入队。可以看到这个方法入队操作非常简单,第5行直接将元素填入数组,该方法没有加锁,入队之后putIndex加一,并唤醒因为队列空而阻塞的线程。为什么没加锁留到put方法说明。
1 private void enqueue(E x) {
2 // assert lock.getHoldCount() == 1;
3 // assert items[putIndex] == null;
4 final Object[] items = this.items;
5 items[putIndex] = x;
6 if (++putIndex == items.length)
7 putIndex = 0;
8 count++;
9 notEmpty.signal();
}
- put方法
由上一篇BlockingQueue的介绍我们知道,put方法必须入队,当时猜想的put方法是要用循环来保证入队,这里也确实如此,只不过把队满作为了循环条件。同步方面,ArrayBlockingQueue用一把锁保证了入队和出队操作同一时刻只能有一个发生。put方法本身已经加锁了,所以enqueue()方法自然也就不用加锁了。
另外:当队列满的时候,await()会释放锁,否则入队出队公用一把锁,poll,take等方法也拿不到锁,会造成死锁。
如果遇到什么情况会死锁这一面试题时,可以回答:同步队列用同一把锁的时候,入队线程持有锁却在等待出队线程出队。这是典型的A在等待B的操作,B又在等待A释放锁。
使用单线程池时,若任务之间有相互关系,也会造成这一情况。
public void put(E e) throws InterruptedException {
checkNotNull(e); //检查e不为空
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();//这里会释放锁,再次唤醒时会获取锁
enqueue(e);//该方法不需要加锁,代码已经实现了同一时刻该方法只会被一个线程调用,且同一时刻不会有出队操作。
} finally {
lock.unlock();
}
}
- offer方法
该方法相比于put方法,唯一区别就是在等待的时候调用了awaitNanos(time),而不是await(),等同于把计算等待多长时间的操作放到了AbstractQueueSynchronizer中完成,AQS实现中断能够定时唤醒用的还是LockSupport.parkNanos(time),LockSupport用的是UNSAFE.park(false, nanos);UNSAFE用的是本地方法,所以这个操作并不是JAVA代码完成的。
另外看过接口介绍的同学应该能够明白为啥offer要返回true false 而put不需要了吧。
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);//检查e不为空
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
Unsafe类中的park方法
出队
- take方法
先看dequeue方法,与enqueue相对应,这个方法也没有加锁,同步逻辑同样是在调用它的take,poll方法来完成的。与插入不同的是,这里引入了一个 itrs.elementDequeued的方法,这是删除迭代器中该元素的操作。我们知道非同步容器ArrayList等在容器变化的时候,迭代器需要抛出ConcurrentModificationException,并发容器么有这一需要,所以elementDequeued方法会把迭代过程中的移除的元素去掉。
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();//迭代器更新
notFull.signal();//通知因队列满而阻塞的线程
return x;
}
与put方法相对应的take方法,while循环直到拿到元素。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await(); //这里会释放锁
return dequeue();//出队
} finally {
lock.unlock();
}
}
- poll方法
与offer方法相对应,逻辑基本一样,这里就不展开了。
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
if (nanos <= 0)
return null;//到时间仍然未获取到元素则返回null。
nanos = notEmpty.awaitNanos(nanos);//释放锁等待固定时长,期间若被唤醒,会获取锁继续循环判断是否有元素可以出队。
}
return dequeue();
} finally {
lock.unlock();
}
}
其他相关方法就不展开了,大家可以自己看源码。
ArrayBlockingQueue可以说是java中最早出现的同步队列了,相比于Collections.synchronizedCollection()的同步快包装实现,它使用了1.5引入的细粒度锁lock,较之以前的synchronize同步块提升了一定的性能。