[Java multithreading] JUC's concurrency container - getting to know concurrency queue BlockingQueue

Posted by fredanthony on Wed, 09 Mar 2022 03:36:26 +0100

preface

My articles related to multithreading
The best learning route of concurrent programming
[Java multithreading] the foundation of high concurrency cultivation. The concept of high concurrency must be understood
[Java multithreading] understand the concepts of thread lock pool and wait pool
[Java multithreading] understand the Java lock mechanism
[Java multithreading] thread communication

[Java basics] multithreading from getting started to mastering - section 15 Using the Concurrent collection
[Java multithreading] JUC's thread pool (I) a preliminary understanding of thread pool section 4 Work queue for thread pool

I JUC

1. Structure diagram of JUC

2.JUC concurrent container

3.JUC thread safe collection

Java. Of Java standard library util. The thread safe collection provided by the concurrent package: ArrayBlockingQueue.

InterfaceThread unsafeThread safety
ListArrayListCopyOnWriteArrayList
MapHashMapConcurrentHashMap
SetHashSet / TreeSetCopyOnWriteArraySet
QueueArrayDeque / LinkedListArrayBlockingQueue / LinkedBlockingQueue
DequeArrayDeque / LinkedListLinkedBlockingDeque

Using these concurrent collections is exactly the same as using non thread safe collection classes. Let's take ConcurrentHashMap as an example:

Map<String, String> map = ConcurrentHashMap<>();
// Read and write in different threads:
map.put("A", "1");
map.put("B", "2");
map.get("A", "1");

Because all synchronization and locking logics are implemented inside the collection, external callers only need to refer to the interface normally, and other codes are exactly the same as the original non thread safe code. When we need multithreading access, that is:

Map<String, String> map = HashMap<>();
//Change to
Map<String, String> map = ConcurrentHashMap<>();

java. util. The collections utility class also provides an old thread safe collection converter that handles List/Set/Map

The syntax is collections synchronizedXXX(Collection c)

Map unsafeMap = new HashMap();
Map threadSafeMap = Collections.synchronizedMap(unsafeMap);
  • It actually wraps a non thread safe Map with a wrapper class, and then locks all read and write methods with synchronized. In this way, the performance of the thread safe collection is better than that of Java util. The concurrent set is much lower, so it is not recommended.

II What is a blocking queue

  • In Java, the interface of BlockingQueue is located in Java util. In the concurrent package (provided from JDK1.5), we can see from the characteristics of the blocking queue described above that the blocking queue is thread safe.

1. JDK provides two implementations on concurrent queues

  • One is a high-performance non blocking queue represented by ConcurrentLinkedQueue,
  • One is the blocking Queue represented by the BlockingQueue interface. No matter which top-level interface is inherited from the Queue.
    • BlockingQueue is an interface. Its implementation classes include ArrayBlockingQueue, LinkedBlockingDeque, PriorityBlockingQueue, DelayQueue, SynchronousQueue, LinkedTransferQueue, LinkedBlockingQueue, etc. the difference of blocking queue is reflected in the difference of storage structure or element operation, but the principle of take and put operation is similar.

Queue summary

  • ArrayDeque: array, double ended queue, non blocking queue
  • PriorityQueue: priority queue, non blocking queue
  • ArrayBlockingQueue: commonly used array based FIFO bounded blocking queue
  • LinkedBlockingQueue: commonly used FIFO based on linked list and optional bounded blocking queue
  • PriorityBlockingQueue, commonly used FIFO unbounded blocking queue with priority,
  • SynchronousQueue common concurrent synchronization blocking queue
  • DelayQueue: delay blocking queue, which implements the BlockingQueue interface
  • LinkedBlockingDeque: FIFO double ended blocking queue based on linked list
  • Concurrent linkedqueue: a lock free non blocking queue based on linked list nodes
  • Concurrent linked deque: lockless two terminal non blocking queue based on linked list nodes

2. The difference between blocking queue and ordinary queue

2 sentence summary

  • 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 that stores the element waits for the queue to become available.

From the article [Java multithreading] thread pool of JUC (6) handwriting blocking queue - Section 1

3. The difference between bounded and unbounded

  • Bounded means that its capacity is limited, that is, its container size must be specified during initialization, and once specified, the capacity size cannot be changed.
  • Unbounded, that is, the capacity size is not specified, and integer is used MAX_ Value is the default capacity of

4.ReentrantLock and Condition

Main methods of ReentrantLock:

  • lock(): get lock
  • Lockinterruptable(): obtain locks and support responding to interrupts
  • tryLock(): when trying to obtain a lock, it returns true if successful. Otherwise, it returns immediately without waiting
  • tryLock(long time,TimeUnit unit): try to obtain a lock within a given time, and support response interruption
  • unlock(): release the lock
  • Condition: through the ReentrantLock instance, the await() and signal() methods correspond to the wait() and notify() of the previous Object respectively

Condition main method

  • await(): the current thread enters the waiting state and releases the lock at the same time. It supports responding to interrupts
  • Awaituninterruptible(): the current thread enters the waiting state and releases the lock at the same time. It does not support responding to interrupts
  • signal(): used to wake up a waiting thread
  • singalAll(): used to wake up all waiting threads

5.BlockingQueue interface

public interface BlockingQueue<E> extends Queue<E> {
	//-----------------Write start----------------------
    //Write the element to the end of the queue and return true successfully. If the queue is full, throw the IllegalStateException("Queue full")
    //If you only write values to a queue of a specified length, the offer() method is recommended.
    boolean add(E e);

    //Write the element to the end of the queue and return true successfully. If the queue is full, return false. The value of e cannot be empty. Otherwise, NullPointerException will be thrown.
    boolean offer(E e);

	//Write the element to the end of the queue. If the queue is full, block the calling thread until the queue has free space
    void put(E e) throws InterruptedException;

    //Write the element to the end of the queue. If the queue is full, block the calling thread for a limited time until the queue has free space or timeout
    boolean offer(E e, long timeout, TimeUnit unit)throws InterruptedException;
	//-----------------Write end----------------------
	

	//-----------------Read start----------------------
    //Read and remove elements from the queue head. If the queue is empty, block the calling thread until an element is written in the queue
    E take() throws InterruptedException;

    //Read and remove elements from the queue head. If the queue is empty, block the calling thread for a limited time until an element is written or timed out in the queue
    E poll(long timeout, TimeUnit unit)throws InterruptedException;
	//-----------------Read end----------------------



	//-----------------Remove element start----------------------
	//Removes the specified value from the queue. If the queue is empty, a NoSuchElementException exception is thrown
    boolean remove(Object o);

    //Remove all the values in the queue and set them to a given set concurrently.
    int drainTo(Collection<? super E> c);

    //Specify the maximum number limit, remove all the values in the queue, and set them concurrently in the given collection.
    int drainTo(Collection<? super E> c, int maxElements);
	//-----------------Remove element end----------------------


    //Gets the space remaining in the queue.
    int remainingCapacity();

    //Determine whether the value is in the queue.
    public boolean contains(Object o);
}

6. Main operation

If the queue is full (insert) or empty (remove)Throw exceptionReturn nowBlocking (response interrupt)Timeout exit (response interrupt)
write inboolean add(E e)boolean offer(e)*void put(E e) *boolean offer(E e, long timeout, TimeUnit unit)*
Read (remove)boolean remove(Object o)E poll() *E take() *E poll(long timeout, TimeUnit unit) *
inspectE element()E peek()N/AN/A
  • Exception:

    • When the queue is full, an IllegalStateException("Queue full") exception will be thrown when an element is written to the queue
    • When the queue is empty, NoSuchElementException will be thrown when reading elements from the queue.
  • Return now:

    • If the element is written successfully, it will return true; otherwise, it will return false.
    • Read the element, return directly if there is data, and return null if there is no data
  • Blocking:

    • When the queue is full, writing elements to the queue will block the producer thread until there is more space in the queue or exit in response to an interrupt.
    • When the queue is empty, reading elements from the queue will block the consumer thread until the queue is available.
  • Timeout exit:

    • When the queue is full, writing elements to the queue will block the producer thread for a certain period of time. If the write timeout, false will be returned, otherwise true will be returned
    • When the queue is empty, reading elements from the queue will block the consumer thread for a certain period of time, and return false when reading timeout, otherwise true

!!! Note: remove(Object o) can remove specific objects from the queue, but this method is not efficient. Because you need to traverse the queue to match a specific object before removing it.

7. How to select an appropriate queue

  1. boundary
  2. space
  3. throughput

III Preliminary use of blocking queue

1. Arrayblockingqueue (array blocking queue)

  • ArrayBlockingQueue: a bounded blocking queue implemented by an array. When initializing, you must specify the container size and store elements in the way of FIFO. ReentrantLock and Condition are used internally, and fair lock and unfair lock are supported.
    • If threads apply for locks in sequence, it is fair, otherwise it is unfair
    • Once the capacity size is set, it cannot be changed

Partial source code

/** Queue container */
final Object[] items;
//Unique global lock: the lock in charge of all access operations. Global sharing. Will use this lock.
final ReentrantLock lock;
//Two waiting queues
/** When the queue is empty, it is used to block the wake-up read-write thread */
private final Condition notEmpty;
/** Used to block wake-up write / read threads when the queue is full */
private final Condition notFull;

//Write element method
public void put(E e) throws InterruptedException {
     checkNotNull(e);
     final ReentrantLock lock = this.lock; // Unique lock
     lock.lockInterruptibly();// Lock
     try {
            while (count == items.length)
                notFull.await();//await gives up the right to operate
            enqueue(e);// Wake up and join the queue.
      } finally {
            lock.unlock();// Unlock
      }
}

//Remove element method
public E take() throws InterruptedException {
     final ReentrantLock lock = this.lock; // Lock
     lock.lockInterruptibly();
     try {
       while (count == 0)//If it is empty, the current lock will be released
           notEmpty.await();
           return dequeue();// Data is returned when the acquired lock is awakened
      } finally {
            lock.unlock();// Release lock
     }
}

Mode of use

    @Test
    public void testArrayBlockingQueue() throws InterruptedException {
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue(5);

        //Producer (add element)
        new Thread(() -> {
            while (true) {
                try {
                    String data = UUID.randomUUID().toString();
                    queue.put(data);
                    System.out.println(Thread.currentThread().getName()+" put: " + data + "-size:" + queue.size());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        //Consumer 1 (remove element)
        new Thread(() -> {
            while (true) {
                try {
                    String data = queue.take();
                    System.out.println(Thread.currentThread().getName() + " take(): " + data + "-size:" + queue.size());
                    Thread.sleep(1200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();


        //Sleep for 100S to prevent the main thread from ending directly
        Thread.sleep(100000);
    }

results of enforcement

2. Linkedblockingqueue (one-way linked list blocking queue)

  • LinkedBlockingQueue: a bounded blocking queue composed of one-way linked list structure. The queue capacity is optional, and the default size is integer MAX_ VALUE. Sort the storage elements according to FIFO., The throughput is usually higher than arrayblockingqueue
    • The atomic class AtomicInteger used for internal counting of this queue
    • The newFixedThreadPool thread pool uses this queue

Partial source code

//Node class for storing data
static class Node<E> {
    E item;
    Node<E> next;
    Node(E x) { item = x; }
}

// The size of the blocking queue. The default is integer MAX_ VALUE 
private final int capacity;
//Number of blocked elements in the current queue 
private final AtomicInteger count = new AtomicInteger();
// Head node of blocking queue
transient Node<E> head;
// Tail node of blocking queue
private transient Node<E> last;
// The lock used to obtain and remove elements, such as take and poll
private final ReentrantLock takeLock = new ReentrantLock();
//  notEmpty condition object, which is used to suspend the thread performing deletion when there is no data in the queue
private final Condition notEmpty = takeLock.newCondition();
// Locks used when adding elements, such as put and offer
private final ReentrantLock putLock = new ReentrantLock();
// notFull condition object, which is used to suspend the added thread when the queue data is full
private final Condition notFull = putLock.newCondition();

The elements written to the LinkedBlockingQueue queue queue will be encapsulated as Node nodes, which will be added to the linked list queue, where the head and last point to the head and tail nodes of the queue respectively. Unlike ArrayBlockingQueue, LinkedBlockingQueue uses takeLock and putLock to control concurrency. In other words, write and remove operations are not mutually exclusive and can be carried out at the same time, which can greatly improve throughput.

Mode of use

@Test
    public void testLinkedBlockingQueue() throws InterruptedException {
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue(5);

        //Producer (add element)
        new Thread( () -> {
            while (true) {
                try {
                    String data = UUID.randomUUID().toString();
                    queue.put(data);
                      System.out.println(Thread.currentThread().getName()+" put: " + data + "-size:" + queue.size());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        //Consumer 1 (remove element)
        new Thread( () -> {
            while (true) {
                try {
                    String data = queue.take();
                    System.out.println(Thread.currentThread().getName() + " take: " + data+"-size:"+queue.size());
                    Thread.sleep(1200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();


        //Sleep for 100S to prevent the main thread from ending directly
        Thread.sleep(100000);
    }

Execution effect:

3. Priority blocking queue

  • PriorityBlockingQueue: it is implemented using balanced binary tree heap and supports unbounded blocking queue sorted by priority. The written object must implement the Comparable interface or pass in the Comparator in the queue construction method. The default is to sort in ascending natural order. The disadvantage is that the order of elements with the same priority cannot be guaranteed. Internally implemented using ReentrantLock and Condition
    • CAS spin lock is used to control the dynamic expansion of the queue to ensure that the expansion operation will not block the execution of take operation
    • Allow writing of null elements
    • Binary reactor classification
      • Maximum heap: the key value of the parent node is always greater than or equal to the key value of any child node
      • Minimum heap: the key value of the parent node is always less than or equal to the key value of any child node
        • The specific performance is as follows: the addition operation is constantly "rising", while the deletion operation is constantly "falling"

Partial source code

  • The construction method is analyzed by priorityblockingqueue (collection <? Extensions E > C), as shown below
/**
 * Construct a queue from an existing set
 * If the existing collection is SortedSet or PriorityBlockingQueue, the original element order is maintained
 */
public PriorityBlockingQueue(Collection<? extends E> c) {
    this.lock = new ReentrantLock();
    this.notEmpty = lock.newCondition();
    boolean heapify = true;     // true if not known to be in heap order
    boolean screen = true;      // true if must screen for nulls
 
    if (c instanceof SortedSet<?>) {                        // If it is an ordered set
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        this.comparator = (Comparator<? super E>) ss.comparator();
        heapify = false;
    } else if (c instanceof PriorityBlockingQueue<?>) {     // If it is a priority queue
        PriorityBlockingQueue<? extends E> pq = (PriorityBlockingQueue<? extends E>) c;
        this.comparator = (Comparator<? super E>) pq.comparator();
        screen = false;
        if (pq.getClass() == PriorityBlockingQueue.class)   // exact match
            heapify = false;
    }
 
    Object[] a = c.toArray();
    int n = a.length;
    if (a.getClass() != Object[].class)
        a = Arrays.copyOf(a, n, Object[].class);
    if (screen && (n == 1 || this.comparator != null)) {    // Verify the existence of null elements
        for (int i = 0; i < n; ++i)
            if (a[i] == null)
                throw new NullPointerException();
    }
    this.queue = a;
    this.size = n;
    if (heapify)    // Heap sort
        heapify();
}
  • Insert element offer() method analysis
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
 
    final ReentrantLock lock = this.lock;   // Lock
    lock.lock();
 
    int n, cap;
    Object[] array;
    while ((n = size) >= (cap = (array = queue).length))    // If the queue is full, expand the capacity
        tryGrow(array, cap);
 
    try {
        Comparator<? super E> cmp = comparator;
        if (cmp == null)    // If the comparator is empty, the heap is adjusted according to the natural order of the elements
            siftUpComparable(n, e, array);
        else                // If the comparator is not empty, adjust the heap according to the comparator
            siftUpUsingComparator(n, e, array, cmp);
        size = n + 1;       // Total queue elements + 1
        notEmpty.signal();  // Wake up an "out of queue thread" that may be waiting
    } finally {
        lock.unlock();
    }
    return true;
}
  • The key methods above are the siftUpComparable() and the siftUpUsingComparator(), which are almost the same internally, except that the former is compared according to the natural order of elements and the latter is compared according to the external comparator. Let's focus on the siftUpComparable() method:
/**
 * Insert the element x into the position of array[k]
 * Then adjust the heap according to the natural order of elements - "floating up" to maintain the "heap" order
 * The end result is a "small top pile"
 */
private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;     // Equivalent to (k-1) divided by 2, which is to find the parent index of K node
        Object e = array[parent];
        if (key.compareTo((T) e) >= 0)  // If the value of the inserted node is greater than the parent node, exit
            break;
 
        // Otherwise, the values of the parent node and the current node are exchanged
        array[k] = e;
        k = parent;
    }
    array[k] = key;
}
  • The function of the siftUpComparable() method is actually the "floating adjustment" of the heap. You can imagine the heap as a complete binary tree. Each inserted element is linked to the bottom right of the binary tree, and then compare the inserted element with its parent node. If the parent node is large, exchange the elements until no parent node is larger than the inserted node. This ensures that the top of the heap (the root node of the binary tree) must be the smallest element. (Note: the above is only for "small top pile")

  • Capacity expansion tryGrow() method

private void tryGrow(Object[] array, int oldCap) {
    lock.unlock();  // Capacity expansion and queue in / out can be carried out at the same time, so release the global lock first
    Object[] newArray = null;
    if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                    0, 1)) {    // allocationSpinLock set to 1 indicates capacity expansion
        try {
            // Calculate new array size
            int newCap = oldCap + ((oldCap < 64) ?
                    (oldCap + 2) :
                    (oldCap >> 1));
            if (newCap - MAX_ARRAY_SIZE > 0) {    // Overflow judgment
                int minCap = oldCap + 1;
                if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                    throw new OutOfMemoryError();
                newCap = MAX_ARRAY_SIZE;
            }
            if (newCap > oldCap && queue == array)
                newArray = new Object[newCap];  // Allocate new array
        } finally {
            allocationSpinLock = 0;
        }
    }
    if (newArray == null)   // Capacity expansion failed (there may be other threads expanding, causing allocationSpinLock contention failure)
        Thread.yield();
    
    lock.lock();            // Get the global lock (because you want to modify the internal array queue)
    if (newArray != null && queue == array) {
        queue = newArray;   // Point to a new internal array
        System.arraycopy(array, 0, newArray, 0, oldCap);
    }
}
  • Analysis of take() method
/**
 * Line up an element If the queue is empty, the thread is blocked
 */
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();   // Get global lock
    E result;
    try {
        while ((result = dequeue()) == null)    // Queue is empty
            notEmpty.await();                   // Thread waiting in noEmpty condition queue
    } finally {
        lock.unlock();
    }
    return result;
}
 
private E dequeue() {
    int n = size - 1;   // n represents the number of remaining elements after the queue
    if (n < 0)          // If the queue is empty, null is returned
        return null;
    else {
        Object[] array = queue;
        E result = (E) array[0];    // array[0] is the top node of the heap. The top node of the heap is deleted every time you leave the queue
        E x = (E) array[n];         // array[n] is the last node of the heap, that is, the lowest right node of the binary tree
        array[n] = null;
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
            siftDownComparable(0, x, array, n);
        else
            siftDownUsingComparator(0, x, array, n, cmp);
        size = n;
        return result;
    }
}

Mode of use

    @Test
    public void testPriorityBlockingQueue() throws InterruptedException {
        //Using the default sorting method, i.e. sorting in natural order (i.e. from small to large), you can implement the Comparable interface through the element or pass it into the Comparator during construction for custom sorting
        PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue(5);
        queue.put(6);
        queue.put(4);
        queue.put(3);
        queue.put(1);
        queue.put(2);
        queue.put(7);

        System.out.println(queue.poll());//1
        System.out.println(queue.poll());//2
    }

results of enforcement

4. Delayqueue

  • DelayQueue: an unbounded blocking queue that internally uses the priority queue PriorityQueue to delay reading. Each element in the queue has an expiration time. When the element is obtained from the queue, only the expired element will be out of the queue. The queue header element is the element whose block will expire. ReentrantLock and Condition are used internally.

    • The written element must implement the Delay interface, specifying the time to get the current element from the queue. Storage elements are sorted by expiration time
      • This interface requires the implementation class to rewrite the compareTo() method and use it in combination with the getDelay() method (specific examples are given below)
    //
    public interface Delayed extends Comparable<Delayed> {
        long getDelay(TimeUnit unit);
    }
    
    • The DelayQueue queue is used inside the newScheduledThreadPool thread pool.
    • Application scenario:
      • 1. Design of cache system: save the validity period of cache elements through DelayQueue, and open a thread to query DelayQueue circularly. Once the elements can be obtained from DelayQueue, it indicates that the validity period of cache has expired
      • 2. Scheduled task scheduling: save the tasks and execution time that will be executed on the day through DelayQueue. Once the task is obtained from DelayQueue, it will be executed. For example, TimerQueue is realized by DelayQueue.)

Partial source code

  • Insert element offer() method
public boolean offer(E e) {
    // Acquire lock
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // Element is added to the priority queue
        q.offer(e);
        // Gets the priority header element, which is equal to the current element
        // Clear the leader and release the read limit
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        return true;
    } finally {
        // Release lock
        lock.unlock();
    }
}
  • Out of queue method take(). If it is empty, the current thread is blocked
public E take() throws InterruptedException {
    // Acquire lock
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        // spin
        for (;;) {
            // Get priority queue header node
            E first = q.peek();
            // Priority queue is empty
            if (first == null)
                // block
                available.await();
            else {
                // Judge whether the remaining time of the header element is less than or equal to 0
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0)
                    // Priority queue out
                    return q.poll();
                // This indicates that the remaining time is greater than 0
                // Null header reference
                first = null;
                // Whether the leader thread is empty. If not, wait
                if (leader != null)
                    available.await();
                else {
                    // Set the leader thread as the current thread
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // Sleep seconds remaining
                        available.awaitNanos(delay);
                    } finally {
                        // After hibernation, is the leader thread still the current thread
                        // Empty leader
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        // The leader thread is empty and first is not empty
        // Wake up the blocked leader and let it try again
        if (leader == null && q.peek() != null)
            available.signal();
        // Unlock
        lock.unlock();
    }
}

Mode of use

//Saved DelayQueue element
public class Item implements Delayed {
    String name;

    //Trigger time
    private long time;

    public Item(String name, long time, TimeUnit unit) {
        this.name = name;
        this.time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0);
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return time - System.currentTimeMillis();
    }

    @Override
    public int compareTo(Delayed o) {
        Item item = (Item) o;
        long diff = this.time - item.time;
        if (diff <= 0) {// Changing to > = will cause problems
            return -1;
        } else {
            return 1;
        }
    }

    @Override
    public String toString() {
        return "Item{" +
                "time=" + time +
                ", name='" + name + '\'' +
                '}';
    }

    @Test
    public void testDelayQueue() throws InterruptedException {
        Item item1 = new Item("item1", 5, TimeUnit.SECONDS);
        Item item2 = new Item("item2", 10, TimeUnit.SECONDS);
        Item item3 = new Item("item3", 15, TimeUnit.SECONDS);

        DelayQueue<Item> queue = new DelayQueue<>();
        queue.put(item1);
        queue.put(item2);
        queue.put(item3);

        System.out.println("begin time:" + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        for (int i = 0; i < 3; i++) {
            Item take = queue.take();
            System.out.format("name:{%s}, time:{%s}\n", take.name, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
        }
        /*
         * begin time:2020-07-07T17:19:53.038
         * name:{item1}, time:{2020-07-07T17:19:57.982}
         * name:{item2}, time:{2020-07-07T17:20:02.982}
         * name:{item3}, time:{2020-07-07T17:20:07.982}
         */
    }
}

Specific example: an order system realizes the delayed consumption of orders through the blocking queue delay function, which requires (Order class, packaging order class, producer, consumer, test)

  • Order class
public class Order {
    //Order number
    private final String orderNo;
    //Order amount
    private final double orderMoney;

    public Order(String orderNo, double orderMoney) {
        super();
        this.orderNo = orderNo;
        this.orderMoney = orderMoney;
    }

    public String getOrderNo() {
        return orderNo;
    }

    public double getOrderMoney() {
        return orderMoney;
    }
}
  • Packaging
// Class description: the element stored in the queue
public class ItemVo<T> implements Delayed {

    private long activeTime;//Expiration time in milliseconds
    private T object;

    //activeTime is an expiration time
    public ItemVo(long activeTime, T object) {
        super();
        this.activeTime = TimeUnit.NANOSECONDS.convert(activeTime, TimeUnit.MILLISECONDS) + System.nanoTime();
        // Convert the incoming duration to the timeout time
        this.object = object;
    }

    public T getObject() {
        return object;
    }

    //Sort by remaining time
    @Override
    public int compareTo(Delayed o) {
        long d = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
        return (d == 0) ? 0 : ((d > 0) ? 1 : -1);
    }

    //Returns the remaining time of the element
    @Override
    public long getDelay(TimeUnit unit) {
        long d = unit.convert(this.activeTime - System.nanoTime(), TimeUnit.NANOSECONDS);
        return d;
    }
}
  • producer
@Slf4j
public class ProducerOrder implements Runnable {
    private DelayQueue<ItemVo<Order>> queue;

    public ProducerOrder(DelayQueue<ItemVo<Order>> queue) {
        super();
        this.queue = queue;
    }

    @SneakyThrows
    @Override
    public void run() {
        while (true) {
            Random random = new Random();
            int num = random.nextInt(5000);//1-5000 random
            
            Order order = new Order("Order-" + num, num);
            ItemVo<Order> itemVo = new ItemVo<Order>(num, order);
            //insert
            queue.offer(itemVo);
            log.info("order" + num + "ms Due after:" + order.getOrderNo());

            TimeUnit.MILLISECONDS.sleep(1500);
        }
    }
}
  • consumer
@Slf4j
public class CustomerOrder implements Runnable {
    private DelayQueue<ItemVo<Order>> queue;

    public FetchOrder(DelayQueue<ItemVo<Order>> queue) {
        super();
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                ItemVo<Order> item = queue.take();
                Order order = (Order) item.getObject();
                log.info("Get expired orders:" + order.getOrderNo());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

  • test
public class DelayQueueTest {
    public static void main(String[] args) throws InterruptedException {
        //Delay queue
        DelayQueue<ItemVo<Order>> queue = new DelayQueue<>();
        //producer
        new Thread(new ProducerOrder(queue)).start();
        //consumer
        new Thread(new CustomerOrder(queue)).start();

        //Print a number every 1 second
        for (int i = 1; i < 1000; i++) {
            Thread.sleep(1000);
            System.out.println(i * 1000);
        }
    }
}

results of enforcement

5. Synchronous queue

  • SynchronousQueue: a blocking queue that does not store elements (no capacity). Each put operation must wait until another thread calls the take operation (that is, the write element must be removed before writing a new element). Otherwise, the write operation is always blocked. Support fair lock (transferqueue FIFO) and unfair lock (transferstack LIFO).
    • Application scenario: the thread pool newCachedThreadPool() uses SynchronousQueue. When a new task arrives in this thread pool, if there is an idle thread, it will use the idle thread to execute (reuse). If there is no idle thread, it will create a new thread and will not cache the task

It is often used to exchange work. Producers and consumers synchronize to deliver certain information, events or tasks

    @Test
    public void testSynchronousQueue() throws InterruptedException {
        SynchronousQueue<String> queue = new SynchronousQueue();

        //Producer (add element)
        new Thread( () -> {
            while (true) {
                try {
                    String data = UUID.randomUUID().toString();
                    System.out.println(Thread.currentThread().getName()+" put: " + data + "-size:" + queue.size());
                    queue.put(data);
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        //Consumer 1 (remove element)
        new Thread( () -> {
            while (true) {
                try {
                    String data = queue.take();
                    System.out.println(Thread.currentThread().getName()+" take: " + data + "-size:" + queue.size());
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        Thread.sleep(100000);
    }

results of enforcement

6. Linkedtransferqueue (linked list transferable blocking queue)

  • LinkedTransferQueue: an unbounded blocking queue composed of linked list structure. Compared with other queues, this class implements the TransferQueue interface and has more transfer() and tryTransfer() methods.

    • If there is a consumer thread waiting for the transfer element, it will directly "hand over" the element to the waiting person; Otherwise, it will put the current element to the end of the queue and enter the blocking state, waiting for the consumer thread to transfer the element.
    • The difference between put() and transfer() methods: put() returns immediately, and transfer() blocks waiting for the consumer to get the data before returning.

    Idea: adopt preemption mode. That is, when the consumer gets the element, if the queue is not empty, the data will be directly taken away. If the queue is empty, a node (node element is null) will be generated to queue (put), and then the consumer will wait on this node. When the producer finds a node with null element, the producer will not queue (no put), and the element will be directly filled into this node, And wake up the waiting thread of the node. The awakened consumer thread takes the element and returns from the called method. We call this node operation as "matching".

public interface TransferQueue<E> extends BlockingQueue<E> {
    // If possible, immediately transfer the element to the waiting consumer. 
    // More specifically, if there is a consumer waiting to receive it (in take or timed poll(long, TimeUnit) pol), the specified element is transmitted immediately, otherwise false is returned.
    //Non blocking
    boolean tryTransfer(E e);

    // Transfer the element to the consumer and wait if necessary. 
    // More precisely, if there is a consumer waiting to receive it (in take or timed poll(long, TimeUnit) pol), the specified element is transmitted immediately, otherwise wait until the element is received by the consumer.
    //Support response interrupt
    void transfer(E e) throws InterruptedException;

    // Set the timeout based on the above method to support the response to interrupt
    boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException;

    // Returns true if at least one consumer is waiting
    boolean hasWaitingConsumer();

    // Returns an estimate of the number of waiting consumers
    int getWaitingConsumerCount();
}

7. Linkedblockingdeque (bidirectional linked list blocking queue)

  • LinkedBlockingDeque: a bounded blocking queue composed of two-way linked lists. The queue capacity is optional, and the default size is integer MAX_ VALUE. Both the head and tail of the queue can write and remove elements, because there is an entry to the operation queue, which reduces the lock competition by half when multiple threads join the queue at the same time

    • Deque property: elements can be inserted and removed at the head and tail of the team
    • Compared with other blocking queues, LinkedBlockingDeque has addFirst(), addLast(), peekFirst(), peekLast() and other methods. The method ending with XXXFirst represents inserting, obtaining and removing the queue header elements of the double ended queue. The method ending with xxxLast means to insert, obtain and remove the tail element of the double ended queue.
    • Scenario: commonly used in work stealing mode

Mode of use

    @Test
    public void testLinkedBlockingDeque (){
        BlockingDeque<String> blockingDeque = new LinkedBlockingDeque<>(1);
        // offer,poll, thread safety / blocking api
        blockingDeque.offer("Add first element");
        String item = blockingDeque.poll();
        System.out.println("poll item:" + item);

        // offer,poll thread safe / throw an exception if failed
        try {
            blockingDeque.put("Add a second element");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        try {
            String take = blockingDeque.take();
            System.out.println("take item:" + take);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Add and remove are not thread safe
        blockingDeque.add("Add a fourth element");
        blockingDeque.add("Add fifth element");
        item = blockingDeque.remove();
        System.out.println(item);
    }

results of enforcement

8. Concurrentlinkedqueue (non blocking one-way linked list queue)

  • ConcurrentLinkedQueue: a queue suitable for high concurrency scenarios. It is an unbounded non blocking thread safe queue based on the linked Node. CAS+volatile is used to achieve thread safety (without blocking function). The storage elements are sorted by FIFO, and the queue does not allow null elements to be written.
    • In general, the performance of ConcurrentLinkedQueue is better than that of BlockingQueue

Mode of use

        // Unbounded queue
        ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
        queue.offer("Zhang San");
        queue.offer("Li Si");
        queue.offer("Wang Wu");
        
        //Get the element from the beginning and delete it
        System.out.println(queue.poll());
        //Get the element from the beginning without deleting it
        System.out.println(queue.peek());
        //Get capacity size
        System.out.println(queue.size());

9. Concurrentlinkeddeque (non blocking bidirectional linked list queue)

ConcurrentLinkedDeque: an unbounded non blocking thread safe queue based on two-way linked list, which is similar to ConcurrentLinkedQueue.

10. Comparison

Blocking lockdata structureIs it boundedThread safetyApplicable scenario
ArrayBlockingQueuearrayBounded. The size cannot be changed after confirmationA ReentrantLock lock controls put and takeProduction and consumption model, balance processing speed
LinkedBlockingQueueUnidirectional linked listConfigurableTwo ReentrantLock locks, put and take, can be executed concurrently.Production and consumption model, balance processing speed
LinkedBlockingDequeBidirectional linked listConfigurableA ReentrantLock lock controls put and take.Production and consumption model, balance processing speed
Priority queue PriorityBlockingQueueBifurcated small top reactorUnbounded, it will expand automaticallyA ReentrantLock lock controls put and take. When the queue is empty, take enters the condition and waits;The verification code in the SMS queue is sent first
Synchronization queueOne way linked list or stackCapacity 1CAS, put and take will be blocked until the pairing is successfulTransfer data between threads
Delay queuePriorityQueue, binary small top pileUnbounded, it will expand automaticallyA ReentrantLock lock controls put and take. Only one thread can take at a time, and other threads enter the condition and wait;Close hyperspace connection, task timeout processing
LinkedTransferQueueUnidirectional linked listunboundedCASPreemption mode: if there is one, take it away directly. If there is no one, occupy this position until it is obtained or timeout or interruption

Topics: Multithreading queue