ArrayBlockingQueue(阻塞队列)
阻塞队列的思想
典型的生产者-消费者模型:
- 当队列数据为空时,所有消费者线程都会被阻塞,等待队列非空。
- 当生产者往阻塞队列填充数据时,队列通知消费者队列非空,消费者开始消费。
- 当阻塞队列因为消费者消费过慢或生产者存放元素过快导致队列填满时无法容纳新元素时,生产者会被阻塞,等待队列非满时继续存放元素。
- 当消费者从队列中消费一个元素之后,队列通知生产者非满,生产者继续填充数据。
源码分析
整体设计
-
实现了阻塞队列
BlockingQueue
这个接口,就拥有了阻塞队列那些常见的操作行为。 -
继承了
AbstractQueue
这个抽象类,拥有了队列的常见操作。
初始化
3个构造方法。
// capacity表示队列初始容量, fair表示锁的公平性
public ArrayBlockingQueue(int capacity, boolean fair) {
//如果设置的队列大小小于0,则直接抛出IllegalArgumentException
if (capacity <= 0)
throw new IllegalArgumentException();
//初始化一个数组用于存放队列的元素
this.items = new Object[capacity];
//创建阻塞队列流程控制的锁
lock = new ReentrantLock(fair);
//用lock锁创建两个条件控制队列生产和消费
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
非公平锁:各个生产者或者消费者线程收到通知后,对于锁的争抢是随机的。
阻塞式获取和新增元素
put(E e)
:将元素插入队列中,如果队列已满,则该方法会一直阻塞,直到队列有空间可用或者线程被中断。take()
:获取并移除队列头部的元素,如果队列为空,则该方法会一直阻塞,直到队列非空或者线程被中断。
public void put(E e) throws InterruptedException {
//确保插入的元素不为null
checkNotNull(e);
//加锁
final ReentrantLock lock = this.lock;
//不调用lock.lock()是为了能够响应中断操作,如果在等待获取锁的过程中被打断则该方法会抛出InterruptedException异常。
lock.lockInterruptibly();
try {
//如果count等数组长度则说明队列已满,当前线程将被挂起放到AQS队列中,等待队列非满时插入(非满条件)。
//在等待期间,锁会被释放,其他线程可以继续对队列进行操作。
while (count == items.length)
notFull.await();
//如果队列可以存放元素,则调用enqueue将元素入队
enqueue(e);
} finally {
//释放锁
lock.unlock();
}
}
private void enqueue(E x) {
//获取队列底层的数组
final Object[] items = this.items;
//将putindex位置的值设置为我们传入的x
items[putIndex] = x;
//更新putindex,如果putindex等于数组长度,则更新为0
if (++putIndex == items.length)
putIndex = 0;
//队列长度+1
count++;
//通知队列非空,那些因为获取元素而阻塞的线程可以继续工作了
notEmpty.signal();
}
public E take() throws InterruptedException {
//获取锁
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//如果队列中元素个数为0,则将当前线程打断并存入AQS队列中,等待队列非空时获取并移除元素(非空条件)
while (count == 0)
notEmpty.await();
//如果队列不为空则调用dequeue获取元素
return dequeue();
} finally {
//释放锁
lock.unlock();
}
}
private E dequeue() {
//获取阻塞队列底层的数组
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//从队列中获取takeIndex位置的元素
E x = (E) items[takeIndex];
//将takeIndex置空
items[takeIndex] = null;
//takeIndex向后挪动,如果等于数组长度则更新为0
if (++takeIndex == items.length)
takeIndex = 0;
//队列长度减1
count--;
if (itrs != null)
itrs.elementDequeued();
//通知那些被打断的线程当前队列状态非满,可以继续存放元素
notFull.signal();
return x;
}
非阻塞式获取和新增元素
offer(E e)
:将元素插入队列尾部。如果队列已满,则该方法会直接返回 false,不会等待并阻塞线程。poll()
:获取并移除队列头部的元素,如果队列为空,则该方法会直接返回 null,不会等待并阻塞线程。add(E e)
:将元素插入队列尾部。如果队列已满则会抛出IllegalStateException
异常,底层基于offer(E e)
方法。remove()
:移除队列头部的元素,如果队列为空则会抛出NoSuchElementException
异常,底层基于poll()
。peek()
:获取但不移除队列头部的元素,如果队列为空,则该方法会直接返回 null,不会等待并阻塞线程。
指定超时时间内阻塞式获取和新增元素
带有等待时间的 offer(E e, long timeout, TimeUnit unit)
和 poll(long timeout, TimeUnit unit)
,用于在指定的超时时间内阻塞式地添加和获取元素。
判断元素是否存在
contains(Object o)
来判断指定元素是否存在于队列中。
常见面试题
ArrayBlockingQueue 是什么?它的特点是什么?
ArrayBlockingQueue
是 BlockingQueue
接口的有界队列实现类,常用于多线程之间的数据共享,底层采用数组实现。
容量有限,一旦创建,容量不能改变。
为了保证线程安全,ArrayBlockingQueue
的并发控制采用可重入锁 ReentrantLock
,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。并且,它还支持公平和非公平两种方式的锁访问机制,默认是非公平锁。
ArrayBlockingQueue
虽名为阻塞队列,但也支持非阻塞获取和新增元素(例如 poll()
和 offer(E e)
方法),只是队列满时添加元素会抛出异常,队列为空时获取的元素为 null,一般不会使用。
ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?
ArrayBlockingQueue
和 LinkedBlockingQueue
是 Java 并发包中常用的两种阻塞队列实现,它们都是线程安全的。
- 底层实现:
ArrayBlockingQueue
基于数组实现,而LinkedBlockingQueue
基于链表实现。 - 是否有界:
ArrayBlockingQueue
是有界队列,必须在创建时指定容量大小。LinkedBlockingQueue
创建时可以不指定容量大小,默认是Integer.MAX_VALUE
,也就是无界的。但也可以指定队列大小,从而成为有界的。 - 锁是否分离:
ArrayBlockingQueue
中的锁是没有分离的,即生产和消费用的是同一个锁;LinkedBlockingQueue
中的锁是分离的,即生产用的是putLock
,消费是takeLock
,这样可以防止生产者和消费者线程之间的锁争夺。 - 内存占用:
ArrayBlockingQueue
需要提前分配数组内存,而LinkedBlockingQueue
则是动态分配链表节点内存。这意味着,ArrayBlockingQueue
在创建时就会占用一定的内存空间,且往往申请的内存比实际所用的内存更大,而LinkedBlockingQueue
则是根据元素的增加而逐渐占用内存空间。
ArrayBlockingQueue 和 ConcurrentLinkedQueue 有什么区别?
ArrayBlockingQueue
和 ConcurrentLinkedQueue
是 Java 并发包中常用的两种队列实现,它们都是线程安全的。
- 底层实现:
ArrayBlockingQueue
基于数组实现,而ConcurrentLinkedQueue
基于链表实现。 - 是否有界:
ArrayBlockingQueue
是有界队列,必须在创建时指定容量大小,而ConcurrentLinkedQueue
是无界队列,可以动态地增加容量。 - 是否阻塞:
ArrayBlockingQueue
支持阻塞和非阻塞两种获取和新增元素的方式(一般只会使用前者),ConcurrentLinkedQueue
是无界的,仅支持非阻塞式获取和新增元素。
ArrayBlockingQueue 的实现原理是什么?
ArrayBlockingQueue
内部维护一个定长的数组用于存储元素。- 通过使用
ReentrantLock
锁对象对读写操作进行同步,即通过锁机制来实现线程安全。 - 通过
Condition
实现线程间的等待和唤醒操作。
线程间的等待和唤醒具体的实现:
- 当队列已满时,生产者线程会调用
notFull.await()
方法让生产者进行等待,等待队列非满时插入(非满条件)。 - 当队列为空时,消费者线程会调用
notEmpty.await()
方法让消费者进行等待,等待队列非空时消费(非空条件)。 - 当有新的元素被添加时,生产者线程会调用
notEmpty.signal()
方法唤醒正在等待消费的消费者线程。 - 当队列中有元素被取出时,消费者线程会调用
notFull.signal()
方法唤醒正在等待插入元素的生产者线程。
Condition:可以实现多路通知功能:在一个
Lock
对象中可以创建多个Condition
实例(即对象监视器)。线程对象可以注册在指定的Condition
中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()
方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock
类结合Condition
实例可以实现“选择性通知”。synchronized 关键字就相当于整个
Lock
对象中只有一个Condition
实例,所有的线程都注册在它一个身上。如果执行notifyAll()
方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而Condition
实例的signalAll()
方法,只会唤醒注册在该Condition
实例中的所有等待线程。