JUC learning - delay queue details

Posted by bossman on Thu, 02 Dec 2021 00:30:43 +0100

1. Basic features of DelayQueue

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E>

DelayQueue delay queue has the characteristics of unbounded queue, blocking queue and priority queue. Let's look at the following separately:

  • Unbounded queue: put the task object to be executed into the queue by calling the offer method (or add method) of DelayQueue. This method is non blocking. This queue is an unbounded queue. When there is enough memory, the number of task objects stored is theoretically unlimited.
  • Blocking queue: DelayQueue implements the BlockingQueue interface and is a blocking queue. However, the queue is only blocked when fetching objects. There are two corresponding methods:
    1. take() method to get and remove the object of the queue header. If the time is not up, it will block the waiting.
    2. poll(long timeout, TimeUnit unit) method, the blocking time length is timeout, and then get and remove the object of the queue header. If the delay time of the object has not reached, it returns null.
  • Priority queue: an important member of DelayQueue is a priority queue PriorityQueue, which is implemented by a binary small top heap. Its feature is that the weight corresponding to the header element is the smallest in the queue, that is, the object obtained through the poll() method is the most priority.
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {

    private final transient ReentrantLock lock = new ReentrantLock();
    private final PriorityQueue<E> q = new PriorityQueue<E>();

2. Delayed interface

The objects stored in the DelayQueue delay queue must be class objects that implement the Delayed interface. The Delayed interface is a subclass of Comparable:

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}

All Delayed interfaces must override their getDelay and compareTo methods.

See an implementation example:

package com.juc.queue;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * @author TyuIn
 * @version 1.0
 * @description
 * @date 2021/12/1 8:41
 */
public class TaskInfo implements Delayed {
    /**
     * Task id
     */
    private int id;
    /**
     * Business type
     */
    private int type;
    /**
     * Business data
     */
    private String data;
    /**
     * execution time
     */
    private long executeTime;

    public TaskInfo(int id, int type, String data, long executeTime) {
        this.id = id;
        this.type = type;
        this.data = data;
        this.executeTime = TimeUnit.NANOSECONDS.convert(executeTime, TimeUnit.MILLISECONDS) + System.nanoTime();
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }

    public long getExecuteTime() {
        return executeTime;
    }

    public void setExecuteTime(long executeTime) {
        this.executeTime = executeTime;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(this.executeTime - System.nanoTime() , TimeUnit.NANOSECONDS);
    }

    @Override
    public int compareTo(@Nonnull Delayed o) {
        return (int) (this.getDelay(TimeUnit.MILLISECONDS) -o.getDelay(TimeUnit.MILLISECONDS));
    }

}

When adding an object through the offer method of DelayQueue, the object will be placed at the specified position in the priority queue according to the compareTo method of the object;

When the object is obtained through the take method of DelayQueue, the getDelay method of the object will be called to determine the delay acquisition time. It should be noted that the time unit here is nanoseconds. In the example code, it is converted through unit.convert (this. Excutetime - system. Nanotime(), timeunit. Nanoseconds).

3. DelayQueue usage example

package com.juc.queue;

import java.util.Random;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author TyuIn
 * @version 1.0
 * @description
 * @date 2021/12/1 8:49
 */
public class DelayQueueTest {
    /**
     * Delay queue
     */
    private static DelayQueue<TaskInfo> queue = new DelayQueue<>();
    /**
     * 3 Thread pool of threads
     */
    private static ExecutorService es =  Executors.newFixedThreadPool(3);

    public static void main(String[] args){
        while (true) {
            try {
                if (queue.size() <= 0){
                    // Get task put in queue
                    getTask();

                    if(queue.size() <= 0){
                        System.out.println("No task sleep for 10 seconds");
                        // No task sleep for 10 seconds
                        TimeUnit.SECONDS.sleep(10);
                    }
                }else{
                    TaskInfo task = queue.take();

                    es.submit(()->{
                        System.out.println("Perform tasks:" + task.getId() + ":" + task.getData());
                    });
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    /**
     * Simulate obtaining tasks to be executed in the next 10 seconds from the database
     */
    public static void getTask(){
        Random r = new Random();
        int t = r.nextInt(2);

        if(t==0){
            return;
        }

        TaskInfo t1 = new TaskInfo(1,1,"Task 1",1000);
        TaskInfo t2 = new TaskInfo(2,2,"Task 2",2000);
        TaskInfo t3 = new TaskInfo(3,3,"Task 3",3000);
        TaskInfo t4 = new TaskInfo(4,4,"Task 4",4000);
        TaskInfo t5 = new TaskInfo(5,5,"Task 5",5000);
        TaskInfo t6 = new TaskInfo(6,6,"Task 6",6000);
        TaskInfo t7 = new TaskInfo(7,7,"Task 7",7000);
        TaskInfo t8 = new TaskInfo(8,8,"Task 8",8000);
        queue.offer(t1);
        queue.offer(t2);
        queue.offer(t3);
        queue.offer(t4);
        queue.offer(t5);
        queue.offer(t6);
        queue.offer(t7);
        queue.offer(t8);
    }

}

Explanation of sample code:

  1. Firstly, a delay queue of DelayQueue is created; And create a thread pool with 3 threads through Executors.newFixedThreadPool(3).
  2. The main method determines in the loop body that if there is no object in the queue, it simulates obtaining the task to be executed in 10 seconds from the database and putting it into the DelayQueue. If there is no task to be executed within 10 seconds in the database, the program sleeps for 10 seconds.
  3. If there are objects in the queue, call the take() method of DelayQueue to obtain the expired task information and hand over the task information to the thread pool for processing.

In the example, the simulation creates 8 tasks, and the delayed execution time of each task is 1 to 8 seconds respectively.

Execute the main method and print a message every 1 second. The complete message is as follows:

Perform tasks: 1:Task 1
 Perform tasks: 2:Task 2
 Perform tasks: 3:Task 3
 Tasks performed: 4:Task 4
 Tasks performed: 5:Task 5
 Tasks performed: 6:Task 6
 Tasks performed: 7:Task 7
 Tasks performed: 8:Task 8

4. Source code analysis of DelayQueue

First, look at the member variables of DelayQueue:

// In order to ensure thread safety, each access operation in the queue needs to be locked, and the re-entry lock is adopted 
private final transient ReentrantLock lock = new ReentrantLock();

// Priority queue: the delayed objects are finally placed in the queue to ensure that the objects taken out from the header each time should be executed first  
private final PriorityQueue<E> q = new PriorityQueue<E>();

// The waiting delay time of the leader thread is the delay time of the highest priority object in the priority queue. Other threads wait indefinitely  
private Thread leader = null;

// It is used with reentry lock to wait and wake up the thread  
private final Condition available = lock.newCondition();

4.1 three queue joining methods

add, offer and put join the queue. add and put directly call the offer method, so calling any of the three methods is equivalent. First, let's look at the offer method:

public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    // Lock  
    lock.lock();
    try {
        // Call the offer method of PriorityQueue and put it into the queue  
        q.offer(e);
        // Judge whether the newly added object is a header node  
        if (q.peek() == e) {
            leader = null;
            // Wake up blocked threads in take() or poll(..) methods  
            available.signal();
        }
        return true;
    } finally {
        // Release lock
        lock.unlock();
    }
}

/**
 * PriorityQueue peek() method in
 */
public E peek() {
    return (size == 0) ? null : (E) queue[0];
}

The most noteworthy thing about this method is that after putting into the queue, judge whether the object just put into the queue is the head node of the PriorityQueue queue. If it is necessary to wake up the waiting block in the take() or poll(..) method, and re obtain the delayed waiting time of the head node object.

The add and put methods call the offer method directly. The source code is:

public boolean add(E e) {
    return offer(e);
}

public void put(E e) {
    offer(e);
}

4.2 four methods of obtaining objects

The four methods poll(), poll(..), take(), peek() can obtain an object from the queue header, but the implementation of each method is different.

(1) peek() method: non blocking method
public E peek() {
    final ReentrantLock lock = this.lock;
    // Lock
    lock.lock();
    try {
        return q.peek();
    } finally {
        // Release lock
        lock.unlock();
    }
}

The peek method of DelayQueue essentially calls the peek method of PriorityQueue, but there is an additional locking operation. This method returns the header node object, but it is not removed from the queue. Peek means a glance.

(2) poll() method: get and remove an object from the queue header, non blocking method
public E poll() {
    final ReentrantLock lock = this.lock;
    // Lock 
    lock.lock();
    try {
        // Gets the header node object in the queue (not removed)  
        E first = q.peek();
        if (first == null || first.getDelay(NANOSECONDS) > 0)
            // Call the getDelay method of the object. If the delay time has not arrived, it will directly return null  
            return null;
        else  // If the delay time has reached, directly call the poll method of fetching and removing the PriorityQueue queue
            return q.poll();
    } finally {
        // Release lock
        lock.unlock();
    }
}

The poll() method first calls the peek method to obtain the header node object, and then determines whether the delay time has arrived by calling the getDelay method of the object. If it has not arrived, it returns null. Otherwise, it calls the poll method of PriorityQueue to take out and remove the header node object and return.

(3) take() method: the core method of DelayQueue. It is often used to delay the execution of tasks. It is a blocking method
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    // Add interrupt lock
    lock.lockInterruptibly();
    try {
        for (;;) {
            // Get header node 
            E first = q.peek();
            if (first == null) 
                // If the head node is empty, release the lock and wait indefinitely. Wait for the offer method to put the object and obtain the lock again
                available.await();
            else {
                // Get delay time of header node object 
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0)
                    // The delay time has expired. Remove it directly from the queue and take it back 
                    return q.poll();
                first = null; // don't retain ref while waiting
                if (leader != null)
                    // If it is not a leader thread, wait indefinitely  
                    available.await();
                else {
                    Thread thisThread = Thread.currentThread();
                    // Set the current thread as the leader thread
                    leader = thisThread;
                    try {
                        // Release the lock, wait for the delay time of the head node, and then obtain the lock.
                        available.awaitNanos(delay);
                    } finally {
                        if (leader == thisThread)
                            // Release leader thread reference
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null)
            // Wake up a thread, obtain the lock, and set the leader thread 
            available.signal();
        // Release lock
        lock.unlock();
    }
}

The main implementation logic of the take method is (for loop body):

  1. Get the header node object. If it is empty, the thread releases the lock and enters an infinite wait. Wait for the offer method to be called. After the object is put in, wake up through the signal() method. [see the source code of the offer method]
  2. If the head node object is not empty, the delay time of the object is obtained. If it is less than 0, the object is directly removed from the queue and returned.
  3. If the delay time of the head node object is greater than 0, judge whether the "leader thread" already exists. If so, it indicates that the current thread is a "follower thread" and enters an indefinite wait (wake up after waiting for the leader thread take method to complete).
  4. If the "leader thread" does not exist, set the current thread to "leader thread", release the lock and wait for the delay time of the head node object, obtain the lock again, and return the next cycle to obtain the head node object.
  5. finally, each time the leader thread executes the take method, it needs to wake up other threads to obtain the lock and become a new leader thread.

The take method implements a thread processing mode of "leader follower mode". Only the leader thread will wait for a specified time to obtain the lock, and other threads will wait indefinitely.

This is why signal is used to wake up in DelayQueue instead of signalAll (only one thread is required to become the leader thread).

This figure shows that three threads call the take method of DelayQueue, and only one thread will become a "leader thread", which is assumed to be thread 1. The other two threads wait indefinitely for the "followers". After the "leader thread" is executed, the signal method is called to wake up a thread randomly to become a new "leader thread".

(4) poll(...) method: the poll method with delay parameter is a blocking method
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    // Convert time to nanoseconds
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    // Add interrupt lock
    lock.lockInterruptibly();
    try {
        for (;;) {
            // Get header node 
            E first = q.peek();
            if (first == null) {
                if (nanos <= 0)
                    return null; // If the specified delay time is less than 0, null will be returned directly  
                else
                    // Wait for the specified delay time before regaining the lock  
                    nanos = available.awaitNanos(nanos);
            } else {
                // Gets the delay time of the header node object 
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0)
                    // If the object delay time has expired, the object is directly fetched and removed, and the
                    return q.poll();
                if (nanos <= 0)
                    // If the object delay time has not expired, but the specified delay time has expired, null is returned
                    return null;
                first = null; // don't retain ref while waiting
                if (nanos < delay || leader != null)
                    // If the specified delay time is less than the object delay time or is not a leader thread
                    // Wake up again after waiting for the specified time.  
                    nanos = available.awaitNanos(nanos);
                else { // If the specified delay time is greater than or equal to the object delay time and the leader thread is empty  
                    Thread thisThread = Thread.currentThread();
                    // Specifies that the current thread is a leader thread  
                    leader = thisThread;
                    try {
                        long timeLeft = available.awaitNanos(delay);
                        // Recalculate the latest "specified delay time"
                        nanos -= delay - timeLeft;
                    } finally {
                        if (leader == thisThread)
                            // Release leader thread reference
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null)
            // Wake up a follower thread after the leader thread finishes executing  
            available.signal();
        // Release lock
        lock.unlock();
    }
}

poll(...) method: if the specified delay time is less than the delay time of the head node object, the return is null and non blocking.

If the specified delay time is greater than the delay time of the head node object, it will be blocked. The blocking length is the delay time of the head node object.

It will be more abstract to say so. Take an example:

package com.juc.queue;

import java.util.concurrent.DelayQueue;
import java.util.concurrent.TimeUnit;

/**
 * @author TyuIn
 * @version 1.0
 * @description
 * @date 2021/12/1 9:40
 */
public class DelayQueueTest1 {

    /**
     * Delay queue
     */
    private static DelayQueue<TaskInfo> queue = new DelayQueue<>();

    public static void main(String[] args) throws Exception{
        // Initialize queue
        getTask();

        // Number of startup threads
        for(int i=0;i<3;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // delay time 
                        TaskInfo task = queue.poll(10000, TimeUnit.MILLISECONDS);

                        if(task == null){
                            System.out.println("Task is empty");
                        }else {
                            System.out.println("Perform tasks:" + task.getId() + ":" + task.getData());
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

    /**
     * Simulate obtaining tasks to be executed in the next 10 seconds from the database
     */
    public static void getTask(){
        TaskInfo t1 = new TaskInfo(1,1,"Task 1",1000);
        TaskInfo t2 = new TaskInfo(2,2,"Task 2",2000);
        TaskInfo t3 = new TaskInfo(3,3,"Task 3",3000);
        TaskInfo t4 = new TaskInfo(4,4,"Task 4",4000);
        TaskInfo t5 = new TaskInfo(5,5,"Task 5",5000);
        TaskInfo t6 = new TaskInfo(6,6,"Task 6",6000);
        TaskInfo t7 = new TaskInfo(7,7,"Task 7",7000);
        TaskInfo t8 = new TaskInfo(8,8,"Task 8",8000);
        queue.offer(t1);
        queue.offer(t2);
        queue.offer(t3);
        queue.offer(t4);
        queue.offer(t5);
        queue.offer(t6);
        queue.offer(t7);
        queue.offer(t8);
    }

}

This instance will start three threads and call the queue.poll(10000, TimeUnit.MILLISECONDS) method at the same time. One thread will be set as the "leader thread", and the waiting time is the delay time of the head node. The waiting time of other threads is 10000ms.

When the "leader thread" is completed, another thread will be selected as the "leader thread" and the waiting time will be changed to the delay time of the current header node.

The main method of executing this code will print a message every 1 second. The complete printing information is as follows:

Perform tasks: 1:Task 1
 Perform tasks: 2:Task 2
 Perform tasks: 3:Task 3

If the specified delay time is changed to 500, i.e. queue.poll(500, TimeUnit.MILLISECONDS), re execute the main() method, and the method returns null. At this time, it will not block, and immediately print three messages:

Task is empty
 Task is empty
 Task is empty

The usage scenario of the poll(...) method is to execute the tasks in the delay queue in batches according to the specified time period.

From the source code point of view, the implementation when the specified delay time is greater than the delay time of the head node object is very similar to the take() method, but the waiting time of the "follower thread" is different: the poll(...) method waits for the specified delay time, and the take() method waits indefinitely.

4.3 other methods

public boolean remove(Object o) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return q.remove(o);
    } finally {
        lock.unlock();
    }
}

public void clear() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        q.clear();
    } finally {
        lock.unlock();
    }
}

Article reference: http://www.itsoku.com/ Bloggers think the content of this article is very good. If you are interested, you can learn about it.

Topics: Java Back-end Multithreading JUC