并发包中的ArrayBlockingQueue和LinkedBlockingQueu源码阅读
ArrayBlockingQueue
- 底层基于数组实现,在对象创建时需要指定数组大小。在构建对象时,已经创建了数组。所以使用 Array 需要特别注意设定合适的队列大小,如果设置过大会造成内存浪费。如果设置内存太小,就会影响并发的性能。
- 功能上,其内部维护了两个索引指针 putIndex 和 takeIndex。putIndex 表示下次调用 offer 时存放元素的位置,takeIndex 表示的时下次调用 take 时获取的元素。
初始化
有三个构造函数,必须设定 队列的大小, 公平和非公平可选。默认情况下不保证线程公平的访问队列,所谓公平访问队列是指阻塞的线程,可以按照阻塞的先后顺序访问队列,即先阻塞线程先访问队列。对于元素而言是FIFO的原则。
构造函数1
public ArrayBlockingQueue(int capacity) {
this(capacity, false); // 默认非公平
}
构造函数2
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();
}
构造函数3
设定从集合中初始化队列,
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair); // 初始化
final ReentrantLock lock = this.lock;
lock.lock(); // 加锁
try {
int i = 0;
try {
for (E e : c) {
checkNotNull(e); // 保证加入的元素不为空
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
添加元素
Offer 和 Add
add方法调用offer,如果添加失败,则抛出异常。
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();
}
}
其中 enqueue 方法的如下:
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x; // 在putIndex位置存放元素
if (++putIndex == items.length) // 更新putindex位置
putIndex = 0;
count++;
notEmpty.signal(); // 通知挂载在notEmpty上的线程,去消费。
}
Put方法
put()方法添加如果不成功则会阻塞。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 加可中断的锁
try {
while (count == items.length) // 如果队列满
notFull.await(); // 释放锁,挂载到notFull条件的等待队列上
enqueue(e); // 入队列
} finally {
lock.unlock();
}
}
还有另外一个offer方法,等待特定时间
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) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos); // 等待特定的纳秒直到超时,则唤醒该线程
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
取出元素
poll()方法
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue(); // 退出队列
} finally {
lock.unlock();
}
}
其中dequeue方法具体如下:
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null; // 指定取出的位置元素为null
if (++takeIndex == items.length) // 取index更新
takeIndex = 0;
count--;
if (itrs != null) // 将所有迭代器中的该元素删除
itrs.elementDequeued();
notFull.signal(); // 通知挂在notFull上的等待线程取获取锁。
return x;
}
take()方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) // 如果数量为空,则将线程挂载到notEmpty等待队列中
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
总结
对于阻塞队列通常提供的方法实现的语义:
方法/处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time, unit) |
检擦方法 | element() | peek() | 不可用 | 不可用 |
🐻 如果是无界阻塞队列,队列不可能会出现满的情况,所以使用put或offer方法永远不会被阻塞,而且使用offer方法时,该方法永远返回true。
ArrayBlockingQueue
是一个基于循环数组实现的有界阻塞队列,通过 putIndex
和 takeIndex
来得到下一个存放和取出的索引,每当索引的位置为数组的长度时,自动更新为1。内置有一把ReentantLock,带有两个条件等待队列,支持的所有方法都通过加锁的方式实现,提供不同的方法(上表所示)应对不同的场景,类似于生产者消费者的有界缓冲区。
LinkedBlockingQueue
- 底层基于单向链表实现。实现了队列的功能,元素到来放到链表尾部,从链表头部取取数据。这种数据结构没有必要使用双向链表。链表的好处(数组的没有的)是不用提前分配内存。Link 也支持在创建对象时指定队列长度,如果没有指定,默认为 Integer.MAX_VALUE。
初始化
默认大小为 Integer.MAX_VALUE;
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
指定设置的阻塞队列大小:
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
通过集合初始化阻塞队列,默认大小 也是最大整形值。
并发支持
private final AtomicInteger count = new AtomicInteger(); // 注意,因为使用了两把锁,使用单独的int类型所以不能保证count更新操作的原子性,因此不像ArrayBlockingQueue一样能保证count只有一个线程操作。
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock(); // 取锁
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition(); // 等待非空的条件
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock(); // 存锁
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition(); // 等待非满的条件
以put()和take()方法为例:
put
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1; // 因为 count 不受锁保护,通过局部变量来保存执行操作后队列容量。
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await(); // 队列满了,进入等待队列
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal(); // 入队成功后,通知等待队列中的其他线程继续执行加入操作。
} finally {
putLock.unlock();
}
if (c == 0) // 如果之前队列为空,且已经执行放入一个的操作,则通知消费者线程去获取!!!这是厉害之处
signalNotEmpty();
}
take()
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await(); // 等待队列非空
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1) // 通知下一个想要取元素的线程去获取元素
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity) // 如果是队列满了之后,第一个执行取出的线程操作,则通知生产者去存放数据。
signalNotFull();
return x;
}
总结
LinkedBlockingQueue
是基于单向链表的有界阻塞队列,默认可以存放大小为Integer.MAX_VALUE, 从链表头取出数据,链表尾部存放数据。支持抽象队列所定义的操作语义。该数据结构内部使用了两把 ReenTrantLock
锁, 一把用来在存放数据使用,一把用来取数据的时候使用,并且各自携带一个等待队列,当存数据但队列满时,线程就进入notFull条件等待队列中;当取数据但队列空时,线程就进入notEmpty条件等待队列中。
有意思的地方在于:
- 对于当前队列的元素数量,使用原子类来进行计数,这是因为两把锁不能保证count的更新一个线程同时进行。
- 当一种操作(存或取)成功时,那么因为某种条件而进入等待队列的线程就会得到通知;如果在队列为空的情况下,所有取操作都会进入到notEmpty等待队列,这时,如果有一个元素存放进去,那么执行操作的线程会接着通知 notEmpty等待队列中的线程执行取数据操作。这是通过内部的局部变量c来实现的!!!
通过这种设置,可以将在链表头上放元素和在链表尾部取元素不再竞争锁,在一定程度上可以加快数据处理。