Blocking Queue in Java Concurrency in Java

This article is forwarded from: http://ifeve.com/java-blocking-queue/

1. What is a blocking queue?

A BlockingQueue is a queue that supports two additional operations. The two additional operations are: when the queue is empty, the thread that gets the element waits for the queue to become non-empty. When the queue is full, the thread storing the element waits for the queue to become available. Blocking queues are often used in producer and consumer scenarios. The producer is the thread that adds elements to the queue, and the consumer is the thread that takes elements from the queue. A blocking queue is a container for producers to store elements, and consumers only get elements from the container.

Blocking queues provide four processing methods:

method\processing method Throw an exception return special value keep blocking time out
Insert method add(e) offer(e) put(e) offer(e,time,unit)
removal method remove() poll() take() poll(time,unit)
Inspection Method element() peek() unavailable unavailable
Throwing an exception: When the blocking queue is full, inserting elements into the queue will throw an IllegalStateException ("Queue full") exception. When the queue is empty, a NoSuchElementException is thrown when an element is retrieved from the queue.
Returns a special value: the insert method will return whether it succeeded, and true if it succeeded. The removal method is to take out an element from the queue. If there is none, return null and block it
all the time: when the blocking queue is full, if the producer thread puts an element into the queue, the queue will block the producer thread until the data is obtained. , or exit in response to an interrupt. When the queue is empty, the consumer thread tries to take elements from the queue, and the queue also blocks the consumer thread until the queue is available.

Timeout exit: When the blocking queue is full, the queue will block the producer thread for a period of time. If it exceeds a certain time, the producer thread will exit.

2. Blocking queue in Java

JDK7 provides 7 blocking queues. ArrayBlockingQueue respectively
: a bounded blocking queue consisting of array structures.
LinkedBlockingQueue : A bounded blocking queue consisting of a linked list structure.
PriorityBlockingQueue : An unbounded blocking queue that supports priority sorting.
DelayQueue: An unbounded blocking queue implemented using a priority queue.
SynchronousQueue: A blocking queue that does not store elements.
LinkedTransferQueue: An unbounded blocking queue consisting of a linked list structure.

LinkedBlockingDeque: A bidirectional blocking queue consisting of a linked list structure.

ArrayBlockingQueue

ArrayBlockingQueue is a bounded blocking queue implemented with arrays. This queue sorts elements on a first-in-first-out (FIFO) basis. By default, fair access to the queue is not guaranteed for visitors. The so-called fair access queue refers to all blocked producer threads or consumer threads. When the queue is available, the queue can be accessed in the order of blocking, that is, the first blocked producer thread. , you can insert elements into the queue first, and the consumer thread that blocks first can get elements from the queue first. Usually throughput is reduced to ensure fairness. We can create a fair blocking queue with the following code:

     ArrayBlockingQueue fairQueue = new  ArrayBlockingQueue(1000,true);

Visitor fairness is implemented using reentrant locks with the following code:

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();
}

LinkedBlockingQueue

LinkedBlockingQueue is a bounded blocking queue implemented with a linked list. The default and maximum length of this queue is Integer.MAX_VALUE. This queue sorts elements on a first-in, first-out basis.

PriorityBlockingQueue

PriorityBlockingQueue is an unbounded queue that supports priorities. By default, elements are arranged in natural order, and the collation of elements can also be specified through the comparator. Elements are sorted in ascending order.

DelayQueue

DelayQueue is an unbounded blocking queue that supports delayed acquisition of elements. Queues are implemented using PriorityQueue. The elements in the queue must implement the Delayed interface, and you can specify how long to get the current element from the queue when you create the element. Elements can be drawn from the queue only when the delay expires. We can use DelayQueue in the following application scenarios:

  • Design of the cache system: DelayQueue can be used to save the validity period of cache elements, and a thread can be used to query DelayQueue cyclically. Once elements can be obtained from DelayQueue, it means that the cache validity period has expired.
  • Timed task scheduling. Use DelayQueue to save the tasks and execution time that will be executed on the day. Once the tasks are obtained from DelayQueue, they will be executed. For example, TimerQueue is implemented using DelayQueue.

Delayed in the queue must implement compareTo to specify the order of elements. For example, let the longest delay time be placed at the end of the queue. The implementation code is as follows:

public int compareTo(Delayed other) {
           if (other == this) // compare zero ONLY if same object
                return 0;
            if (other instanceof ScheduledFutureTask) {
                ScheduledFutureTask x = (ScheduledFutureTask)other;
                long diff = time - x.time;
                if (diff < 0)
                    return -1;
                else if (diff > 0)
                    return 1;
       else if (sequenceNumber < x.sequenceNumber)
                    return -1;
                else
                    return 1;
            }
            long d = (getDelay(TimeUnit.NANOSECONDS) -
                      other.getDelay(TimeUnit.NANOSECONDS));
            return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
        }

How to implement the Delayed interface

We can refer to the ScheduledFutureTask class in ScheduledThreadPoolExecutor. This class implements the Delayed interface. First of all: When the object is created, use time to record when the object can be used. The code is as follows:

ScheduledFutureTask(Runnable r, V result, long ns, long period) {
            super(r, result);
            this.time = ns;
            this.period = period;
            this.sequenceNumber = sequencer.getAndIncrement();
}

Then use getDelay to query how long the current element needs to be delayed. The code is as follows:

public long getDelay(TimeUnit unit) {
            return unit.convert(time - now(), TimeUnit.NANOSECONDS);
        }

It can be seen from the constructor that the unit of the delay time parameter ns is nanoseconds. It is best to use nanoseconds when designing your own, because you can specify any unit when getDelay. Once the unit is nanoseconds, the delay time is less accurate. Nanoseconds are troublesome. Please note that when time is less than the current time, getDelay will return a negative number.

How to implement a delay queue

The implementation of the delay queue is very simple. When the consumer gets an element from the queue, if the element does not reach the delay time, the current thread is blocked.

long delay = first.getDelay(TimeUnit.NANOSECONDS);
                    if (delay <= 0)
                        return q.poll();
                    else if (leader != null)
                        available.await();

SynchronousQueue

SynchronousQueue is a blocking queue that does not store elements. Each put operation must wait for a take operation, otherwise it cannot continue to add elements. SynchronousQueue can be seen as a passer, responsible for passing the data processed by the producer thread directly to the consumer thread. The queue itself does not store any elements, which is very suitable for transitive scenarios, such as data used in one thread, passed to another thread for use, and the throughput of SynchronousQueue is higher than that of LinkedBlockingQueue and ArrayBlockingQueue.

LinkedTransferQueue

LinkedTransferQueue is an unbounded blocking TransferQueue queue consisting of a linked list structure. Compared with other blocking queues, LinkedTransferQueue has more tryTransfer and transfer methods.

transfer method. If there is currently a consumer waiting to receive an element (when the consumer uses the take() method or the poll() method with a time limit), the transfer method can transfer the element passed in by the producer to the consumer immediately. If there is no consumer waiting to receive the element, the transfer method will store the element in the tail node of the queue and wait until the element is consumed by the consumer before returning. The key code of the transfer method is as follows:

Node pred = tryAppend (s, haveData);
return awaitMatch(s, pred, e, (how == TIMED), nanos);

The first line of code attempts to use the s node that holds the current element as the tail node. The second line of code is to make the CPU spin and wait for the consumer to consume the element. Because spinning consumes CPU, use the Thread.yield() method after spinning a certain number of times to suspend the currently executing thread and execute other threads.

tryTransfer method. It is used to test whether the elements passed in by the producer can be directly passed to the consumer. Returns false if no consumers are waiting to receive the element. The difference from the transfer method is that the tryTransfer method returns immediately regardless of whether the consumer receives it or not. The transfer method must wait until the consumer consumes it before returning.

For the tryTransfer(E e, long timeout, TimeUnit unit) method with a time limit, it tries to pass the element passed in by the producer to the consumer directly, but if no consumer consumes the element, it will wait for the specified time before returning , returns false if the element has not been consumed within the timeout period, and returns true if the element is consumed within the timeout period.

LinkedBlockingDeque

LinkedBlockingDeque is a bidirectional blocking queue consisting of a linked list structure. The so-called two-way queue means that you can insert and remove elements from both ends of the queue. Due to the addition of an entry to the operation queue, the double-ended queue reduces the competition by half when multiple threads enter the queue at the same time. Compared with other blocking queues, LinkedBlockingDeque has more methods such as addFirst, addLast, offerFirst, offerLast, peekFirst, peekLast, etc. The methods ending with the word First mean insert, get (peek) or remove the first element of the double-ended queue. A method ending with the word Last that inserts, gets, or removes the last element of the deque. In addition, the insertion method add is equivalent to addLast, and the removal method remove is equivalent to removeFirst. But the take method is equivalent to takeFirst. I don't know if it is a bug of Jdk. It is clearer to use the method with the First and Last suffixes. The capacity of the queue can be initialized when the LinkedBlockingDeque is initialized to prevent excessive expansion when it is re-expanded. In addition, bidirectional blocking queues can be used in "work stealing" mode.

3. Implementation principle of blocking queue

If the queue is empty, the consumer will keep waiting. When the producer adds an element, how does the consumer know that the current queue has an element? If you were to design a blocking queue, how would you design it so that producers and consumers can communicate efficiently? Let's first take a look at how the JDK is implemented.

Implemented using the notification pattern. The so-called notification mode means that when the producer adds elements to the full queue, the producer will be blocked, and when the consumer consumes an element in the queue, the producer will be notified that the current queue is available. By looking at the JDK source code, it is found that ArrayBlockingQueue is implemented using Condition. The code is as follows:

private final Condition notFull;
private final Condition notEmpty;

public ArrayBlockingQueue(int capacity, boolean fair) {
        //Omit other code
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }
 
public void put(E e) throws InterruptedException {
        checkNotNull (e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            insert(e);
        } finally {
            lock.unlock();
        }
}
 
public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return extract();
  } finally {
            lock.unlock();
        }
}
 
private void insert(E x) {
        items [putIndex] = x;
        putIndex = inc (putIndex);
        ++count;
        notEmpty.signal();
    }

When we insert an element into the queue, if the queue is unavailable, blocking the producer is mainly implemented by LockSupport.park(this);

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter ();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
 
reportInterruptAfterWait(interruptMode);
        }

Continue to enter the source code and find that calling setBlocker first saves the thread to be blocked, and then calls unsafe.park to block the current thread.

public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        unsafe.park(false, 0L);
        setBlocker(t, null);
    }

unsafe.park is a native method, the code is as follows:

    public native void park(boolean isAbsolute, long time);

The park method blocks the current thread and returns only when one of the following four conditions occurs.

  • When unpark corresponding to park executes or has already executed. Note: Executed refers to the park that unpark executes first and then executes.
  • when the thread is interrupted.
  • If the time in the parameter is not zero, wait the specified number of milliseconds.
  • When an abnormal phenomenon occurs. These anomalies cannot be determined in advance.

Let's continue to see how the JVM implements the park method. Park is implemented in different ways in different operating systems. Under Linux, the system method pthread_cond_wait is used. Implement the os::PlatformEvent::park method in the JVM source code path src/os/linux/vm/os_linux.cpp. The code is as follows:

void os::PlatformEvent::park() {
             int v ;
         for (;;) {
        v = _Event ;
         if (Atomic::cmpxchg (v-1, &_Event, v) == v) break ;
         }
         guarantee (v >= 0, "invariant") ;
         if (v == 0) {
         // Do this the hard way by blocking ...
         int status = pthread_mutex_lock(_mutex);
         assert_status(status == 0, status, "mutex_lock");
         guarantee (_nParked == 0, "invariant") ;
         ++ _nParked ;
         while (_Event < 0) {
         status = pthread_cond_wait(_cond, _mutex);
         // for some reason, under 2.7 lwp_cond_wait() may return ETIME ...
         // Treat this the same as if the wait was interrupted
         if (status == ETIME) { status = EINTR; }
         assert_status(status == 0 || status == EINTR, status, "cond_wait");
         }
         -- _nParked ;
 
         // In theory we could move the ST of 0 into _Event past the unlock(),
         // but then we'd need a MEMBAR after the ST.
         _Event = 0 ;
         status = pthread_mutex_unlock(_mutex);
         assert_status(status == 0, status, "mutex_unlock");
         }
         guarantee (_Event >= 0, "invariant") ;
         }
 
     }

pthread_cond_wait is a multi-threaded condition variable function, cond is the abbreviation of condition, the literal meaning can be understood as the thread is waiting for a condition to occur, this condition is a global variable. This method takes two parameters, a shared variable _cond and a mutex _mutex. The unpark method is implemented using pthread_cond_signal under linux. park is implemented under Windows using WaitForSingleObject.

When the queue is full, the producer inserts an element into the blocking queue, and the producer thread will enter the WAITING (parking) state. We can see this with a blocked producer thread using jstack dump:

"main" prio=5 tid=0x00007fc83c000000 nid=0x10164e000 waiting on condition [0x000000010164d000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
 - parking to wait for  <0x0000000140559fe8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2043)
        at java.util.concurrent.ArrayBlockingQueue.put(ArrayBlockingQueue.java:324)
        at blockingqueue.ArrayBlockingQueueTest.main(ArrayBlockingQueueTest.java:11)

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325898985&siteId=291194637