Producer consumer model --BlockingQueue talk in Java

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:

  1. Resources needed to ensure the visibility of the thread, at the same time you want to manually implement thread synchronization
  2. We need to consider a variety of critical situations and denial policy
  3. 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:

figure 1

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 putoperation (even when using a offermethod instead putcalled a method), removing elements of takethe 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:

figure 2

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 peekfor 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 elementmethod and the peekmethod 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: ReentranLockand Condition. Which ReentranLockis based on the AQS [2] a reentrant lock implementation (not understand reentrant concept can be used as an ordinary lock), Conditiona wait / notification particular implementation mode (as it will be appreciated a method of providing a more powerful the waitand notifyclasses)

countProperties of natural Needless to say, headand lastit 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 ReentrantLockand Conditionthe 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 Conditionthis category. In fact, Conditionan 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/notifywhich is the method of managing object locks and lock type , they are waiting for locks manipulation queue of threads; and await/signalthese processes are managed AQS-based lock wait queue manipulation of naturally AQS thread

So here's notEmptyto maintain a waiting take锁thread queue, notFullmaintaining a waiting put锁thread queue. Also well understood in the literal sense, notEmptymeans "queue not empty", so the elements can take the same token, notFullit 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, offerthe method will return the user false, similar to the way non-blocking communication. offerThread-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 takeoperation. Many people may wonder why should we increase a judge here, it is that, throughout the process, cthe 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 0that 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 takeoperation [3]

At this point, the entire process flow can be summarized as follows:

  1. Obtainput锁
  2. Elements into the team, and increment countvalue
  3. If the capacity is not reached the upper limit, a wake-up putoperation
  4. If the queue is empty, a wake-up at the last element before the insertion takeoperation

offer(e, timeout, unit)

Build on the progress we then look out mechanism with the offermethod:

    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:

  1. Acquiring the lock uses interruptible form, ieputLock.lockInterruptibly()
  2. If the queue is always full, it will loop executes notFull.awaitNanos(nanos)operations to add the current thread to notFullwait in the queue (wait putoperation to be performed)

And the rest of offer(e)exactly the same, not go into details here

add(e)

addThe method and offercompared 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 AbstractQueuethe

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, awaitthe operation is no longer the timeout, that is to say, can only wait for the takeoperation [4] to invoke the signalmethod 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 AbstractQueuethe:

    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, LinkedBlockingQueuethe 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 LinkedBlockingQueuethrough 生产者锁/put锁and 消费者锁/take锁, as well as the corresponding lock Conditionto achieve thread-safe objects. I understand this point, in order to understand the whole生产者/消费者模型


  1. Here reference is made to "Java concurrent programming Art" ↩︎

  2. See On the AQS (abstract queue synchronizer) article ↩︎

  3. Here described as "a wake-up takeoperation" somewhat inaccurate, actual should be described as "a wake up waiting take锁threads," but I think the former is more help readers to understand, so follow the former way to describe ↩︎

  4. And it refers to a takefunctional group of a similar method, comprising take/poll/remove, putoperating the same way ↩︎

Guess you like

Origin juejin.im/post/5cf71ffb51882524156c9b8d