Foreword
Producer / consumer model I am sure you are not familiar, is a very common distributed resource scheduling model. In this model, there are at least two objects: producers and consumers. Producers only responsible for creating resources, consumers only responsible use of resources. If they achieve a simple producer / consumer model is also very easy, by nothing more than a queue to do, but this approach has many hidden defects:
- Resources needed to ensure the visibility of the thread, at the same time you want to manually implement thread synchronization
- We need to consider a variety of critical situations and denial policy
- The need to maintain a balance between throughput and thread-safe
So Java has been ahead of us a good package of interface and implementation, then we will BlockingQueue for the interface and its implementation class used a brief analysis LinkedBlockingQueue
Blocking queue
concept
BlockingQueue, meaning blocking queue, we can see from the class definition, it inherited the Queue interface, so it can be used as a queue:
Now called the blocking queue, that queue is blocking this operation way, embodied in the following two aspects:
- Inserted element is blocking operation: when the queue is full, the thread insertion operation is performed blocked
- When removing operation blocking elements: the thread when the queue is empty, execution removing operation is blocked
In this way, it can easily coordinate the relationship between producers and consumers
Interface Method
In BlockingQueue, the following six interfaces are defined:
public interface BlockingQueue<E> extends Queue<E> {
boolean add(E e);
boolean offer(E e);
void put(E e) throws InterruptedException;
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
E take() throws InterruptedException;
E poll(long timeout, TimeUnit unit) throws InterruptedException;
int remainingCapacity();
boolean remove(Object o);
public boolean contains(Object o);
int drainTo(Collection<? super E> c);
int drainTo(Collection<? super E> c, int maxElements);
}
复制代码
The interface method according to the functions can be divided into three categories:
- Add elements include: add, offer, put
- Remove elements include: remove, poll, take, drainTo
- Gets / check elements include: contains, remainingCapacity
In general, we will also add an element called the put
operation (even when using a offer
method instead put
called a method), removing elements of take
the operation
For the first two categories, exception handling can follow the way again into the following categories:
- Throws an exception: add, remove
- Returns special value: offer (e), poll
- Blocking: put (e), take
- Timeout Exit: offer (e, time, unit), poll (time, unit)
These types of treatment I will not explain, it is clear that the literal meaning has been
Realization of blocking queue
JDK8 provides the following BlockQueue implementation class:
We used the basic are the following:
- ArrayBlockingQueue: ArrayList based implementation blocking queue, bounded
- LinkedBlockingQueue: Based on LinkedList implement blocking queue, bounded
- PriorityBlockingQueue: priority queue, unbounded
- DelayQueue: Support delay elements get priority queue, unbounded
The remaining interest can achieve self-understanding, we are here to LinkedBlockingQueue for example, describes how Java is to achieve blocking queue
Interface Method
In addition to the interface method BlockingQueue provided, LinkedBlockingQueue also provides a method peek
for obtaining the first team node
At this point, we used the blocking queue methods have been explained finished here with a table to summarize [1] :
The method / approach | Throw an exception | Returns the special value | Clog | Timeout exit |
---|---|---|---|---|
Insert elements | add(e) | offer(e) | put(e) | offer(e, timeout, unit) |
Remove elements | remove() | poll() | take() | poll(timeout, unit) |
Gets the element | element() | peek() | / | / |
Wherein the element
method and the peek
method of functionally identical
Attributes
BlockingQueue only defines the interface specification, the true realization is done by a specific category, Let us skip the middle AbstractQueue, directly to research LinkedBlockingQueue, where several important domain object:
/** 元素个数 */
private final AtomicInteger count = new AtomicInteger();
/** 队首节点 */
transient Node<E> head;
/** 队尾节点 */
private transient Node<E> last;
/** take、poll等方法持有的锁,这里叫做take锁或出锁 */
private final ReentrantLock takeLock = new ReentrantLock();
/** take方法的等待队列 */
private final Condition notEmpty = takeLock.newCondition();
/** put、offer等方法持有的锁,这里叫做put锁或入锁 */
private final ReentrantLock putLock = new ReentrantLock();
/** put方法的等待队列 */
private final Condition notFull = putLock.newCondition();
复制代码
Node node is an ordinary queue node, and LinkedList, we focus on four domain objects behind can be divided into two categories: for inserting elements, and for removing elements. Wherein each class has two attributes: ReentranLock
and Condition
. Which ReentranLock
is based on the AQS [2] a reentrant lock implementation (not understand reentrant concept can be used as an ordinary lock), Condition
a wait / notification particular implementation mode (as it will be appreciated a method of providing a more powerful the wait
and notify
classes)
count
Properties of natural Needless to say, head
and last
it is clear that is used to maintain the queue storage elements, I believe that they do not elaborate. Blocking queue and ordinary differentiating point is that the back of the queue ReentrantLock
and Condition
the type of four properties, about the meaning of these four properties, in the next few modules will conduct in-depth analysis
In order to facilitate the next but we explain, let's briefly explain Condition
this category. In fact, Condition
an interface, a specific category in the AQS. For this article, you only need to know three methods: await()
, , signal()
and singalAll()
. These three methods can be entirely analogous wait()
, notify()
and notifyAll()
the difference between them may be understood as a blur, wait/notify
which is the method of managing object locks and lock type , they are waiting for locks manipulation queue of threads; and await/signal
these processes are managed AQS-based lock wait queue manipulation of naturally AQS thread
So here's notEmpty
to maintain a waiting take锁
thread queue, notFull
maintaining a waiting put锁
thread queue. Also well understood in the literal sense, notEmpty
means "queue not empty", so the elements can take the same token, notFull
it means "not full queue", you can insert elements inside
Insert elements
offer(e)
First look at offer(e)
the method, the following source code:
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
// 如果容量达到上限会返回false
if (count.get() == capacity)
return false;
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
// 获取put锁
putLock.lock();
try {
if (count.get() < capacity) {
// 入队并自增元素个数
enqueue(node);
// 注意,这里c返回的是增加前的值
c = count.getAndIncrement();
// 如果容量没到上限,就唤醒一个put操作
if (c + 1 < capacity)
notFull.signal();
}
} finally {
// 解锁
putLock.unlock();
}
if (c == 0)
// 如果队列之前为空,会唤醒一个take操作
signalNotEmpty();
return c >= 0;
}
复制代码
This method is most operations are well understood, when you add the element of operation is not allowed, offer
the method will return the user false
, similar to the way non-blocking communication. offer
Thread-safe approach is through put锁
to ensure that the
There is a very interesting place, if we look at the last judgment c == 0
, it will wake up a take
operation. Many people may wonder why should we increase a judge here, it is that, throughout the process, c
the initial value is -1
, modify its value is the only place c = count.getAndIncrement()
this statement. In other words, if it is determined c == 0
, then the return value of this statement is 0
that before inserting elements, the queue is empty. Therefore, if a start queue is empty, when the first element is inserted, a will immediately wake up take
operation [3]
At this point, the entire process flow can be summarized as follows:
- Obtain
put锁
- Elements into the team, and increment
count
value - If the capacity is not reached the upper limit, a wake-up
put
operation - If the queue is empty, a wake-up at the last element before the insertion
take
operation
offer(e, timeout, unit)
Build on the progress we then look out mechanism with the offer
method:
public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
// 可被中断地获取put锁
putLock.lockInterruptibly();
try {
// 重复执行while循环体,直到队列不满,或到了超时时间
while (count.get() == capacity) {
// 到了超时时间后就返回false
if (nanos <= 0)
return false;
// 会将当前线程添加到notFull等待队列中,
// 返回的是剩余可用的等待时间
nanos = notFull.awaitNanos(nanos);
}
enqueue(new Node<E>(e));
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return true;
}
复制代码
And the overall process is substantially offer(e)
the same procedure, there are two different points:
- Acquiring the lock uses interruptible form, ie
putLock.lockInterruptibly()
- If the queue is always full, it will loop executes
notFull.awaitNanos(nanos)
operations to add the current thread tonotFull
wait in the queue (waitput
operation to be performed)
And the rest of offer(e)
exactly the same, not go into details here
add(e)
add
The method and offer
compared with a method, when the operation is not allowed, an exception is thrown instead of returning a special value, as follows:
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
复制代码
Simply is to offer(e)
do a second package, nothing to say, you need to mention the implementation of this method is that it is important in AbstractQueue
the
put(e)
put(e)
When the method of operation will be allowed to block a thread, we look at it is how to achieve:
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
// 以可中断的形式获取put锁
putLock.lockInterruptibly();
try {
// 与offer(e, timeout, unit)相比,采用了无限等待的方式
while (count.get() == capacity) {
// 当执行了移除元素操作后,会通过signal操作来唤醒notFull队列中的一个线程
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
复制代码
Sure enough, between the methods are similar, put(e)
the operation can be compared before we speak offer(e, timeout, unit)
, only a different place, that is, when the queue is full, await
the operation is no longer the timeout, that is to say, can only wait for the take
operation [4] to invoke the signal
method wake up the thread
Remove elements
poll()
poll()
Method for the removal and return to the first team node, the following is a specific implementation of the method:
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
// 获取take锁
takeLock.lock();
try {
if (count.get() > 0) {
// 出队,并自减
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
// 只要队列还有元素,就唤醒一个take操作
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
// 如果在队列满的情况下移除一个元素,会唤醒一个put操作
if (c == capacity)
signalNotFull();
return x;
}
复制代码
If you carefully read the offer(e)
following methods, poll()
methods have nothing to speak of, is entirely offer(e)
a replica (I also want to say something, but the poll()
method is completely and offer(e)
process exactly the same ...)
other
poll(timeout, unit)/take()/remove()
Methods are offer(e, timeout, unit)/put()/add()
replica method, there is no special place, here sum skipped
Gets the element
peek()
peek()
The method is used to get the team to the first element, which is implemented as follows:
public E peek() {
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
// 获取take锁
takeLock.lock();
try {
Node<E> first = head.next;
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock();
}
}
复制代码
Process nothing to say, should be noted that the method needs to obtain take锁
, that is to say at the peek()
time of execution method, it is unable to perform the operation to remove the element of
element()
element()
Implementation of the method is in AbstractQueue
the:
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
复制代码
Or the same secondary packaging operations
to sum up
This is it BlockingQueue
, the results say for a long time LinkedBlockingQueue
. However, as a classical blocking queue implementation, LinkedBlockingQueue
the method of realization of ideas is also very important for understanding the blocking queue for the. Want to understand the concept of blocking queue, the most important thing is to understand the concept of locks, such as LinkedBlockingQueue
through 生产者锁/put锁
and 消费者锁/take锁
, as well as the corresponding lock Condition
to achieve thread-safe objects. I understand this point, in order to understand the whole生产者/消费者模型
Here reference is made to "Java concurrent programming Art" ↩︎
See On the AQS (abstract queue synchronizer) article ↩︎
Here described as "a wake-up
take
operation" somewhat inaccurate, actual should be described as "a wake up waitingtake锁
threads," but I think the former is more help readers to understand, so follow the former way to describe ↩︎And it refers to a
take
functional group of a similar method, comprisingtake/poll/remove
,put
operating the same way ↩︎