JavaEE (series 10) — multithreading case 3 (timer)

Directory

1. Timer

2. Timers in the standard library

3. Implement the timer

3.1 Create a priority blocking queue

3.2 Create MyTask class

3.3 Build the schedule method

3.4 Build threads in the timer class

3.5 Thinking


1. Timer

Timer is also an important component in software development. It is similar to an “alarm clock”. After a set time is reached, a specified code will be executed.

2. Timers in the standard library

  1. The standard library provides a Timer class. The core method of the Timer class is schedule.
  2. The schedule contains two parameters. The first parameter specifies the task code to be executed, and the second parameter specifies how long it will take to execute (in milliseconds).

Steps for usage:

1. Instantiate the Timer object

2. Call timer.schedule(“task”, execution time)

public class timerTest {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello4");
            }
        },4000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello3");
            }
        },3000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello2");
            }
        },2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello1");
            }
        },1000);
        System.out.println("hello");
    }
}

Running results: first print the hello of the main process, and then print the content of each thread successively according to the specified time

3. Implement timer

The composition of the timer

  1. A priority blocking queue
  2. Each element in the queue is a Task object
  3. Task has a time attribute, and the first element of the queue is the task to be executed
  4. At the same time, a worker thread keeps scanning the first element of the queue to see if the first element of the queue needs to be executed

Why use a priority blocking queue?

Answer: Because the tasks in the blocking queue have their own execution time (delay). The task executed first must be the task with the smallest delay. Using a queue with priority can efficiently find out the task with the smallest delay .

3.1 Create a priority blocking queue

3.2 Create MyTask class

Each element in the queue is a Task object, and the Mytask class is created to describe the task to be executed and the execution time. (for passing in the blocking queue)

Note here:

We need to implement the Comparable interface of the Mytask class and compare it according to the execution time. Only in this way can we pass in the blocked queue with priority.

The final MyTask class code is:

class MyTask implements Comparable<MyTask>{
    public Runnable runnable;
    public long time;

    public MyTask(Runnable runnable, long delay){
        // Get the timestamp of the current moment + delay = the timestamp of the actual execution of the current task
        this.time = System.currentTimeMillis() + delay;
        this.runnable = runnable;
    }


    @Override
    public int compareTo(MyTask o) {
        // Each time the element with the smallest time is taken out
        return (int)(this.time -o.time);
    }
}

3.3 Build schedule method

Insert Task objects into the queue through the schedule method

3.4 Build threads in timer class

There is a worker thread in the Timer class, which keeps scanning the first element of the queue to see if it can perform this task.

The implementation idea of this thread

  • 1. The thread needs to continuously take out tasks from the queue queue.take();
  • 2. Take out the task and compare the current system time with the task execution time
  • 3. If the task execution time is less than the current system time, it means that the task is to be executed. Call myTask.runnable.run();
  • 4. If the execution time of the currently retrieved task is greater than the current system time, it means that the task has not yet reached the execution time, and the task will be pushed to the queue. At the same time, it will enter the block waiting
  • 5. In the schedule method, after the task is pushed to the priority queue, a notify method is added at the same time to wake up the thread that is being blocked at this time, so that the blockage is waiting to be resolved, and the first task of the queue is re-taken to compare the time.

The advantage of adding wait notify is that the work thread does not need to keep fetching the first element of the queue, which will consume system resources and cause unnecessary waste. It only needs to wait for the time difference between the current distance to execute the task and block it. When a new task is added When the contact is blocked, recalculate the time difference, and then decide whether to execute the task or enter the blocked state.

public class MyTimer {
    private final PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    // Create a lock object
    private final Object locker = new Object();
    public MyTimer(){
        //1. Create a thread
        Thread work = new Thread(()->{
           while (true) {
               try {
                   synchronized (locker) {
                       //2. Take out a task from the queue
                       MyTask myTask = queue. take();
                       //3 Get the current time
                       long curTime = System. currentTimeMillis();
                       //4. Compare the task execution time with the current time
                       if (myTask.time <= curTime){
                           //4.1 The task execution time is less than the current time, indicating that the task should be executed
                           myTask.runnable.run();
                       } else {
                           //4.1 The task execution time is greater than the current time, indicating that the task has not yet reached the execution time, and then put the task that was just taken back into the original queue
                           queue. put(myTask);
                           locker.wait(myTask.time-curTime);

                           //For this wait():
                           //1. It is convenient to wake up at any time, for example, the current time is 14:00, and it is agreed to perform class tasks at 14:30,
                           //At this time, take out the first element of the queue, and find that the time has not arrived, just wait (task execution time-current time)
                           //2. When a new task comes, it needs to be executed earlier than the previous team, then it needs to wake up the wait() before,
                           //Retake the first element of the team, compare the time, and determine the time of wait().

                       }
                   }
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
        work. start();
    }

    public void schedule(Runnable runnable, long delay){
        // According to the parameters, construct MyTask and insert into the queue
        MyTask myTask = new MyTask(runnable, delay);
        queue. put(myTask);
        synchronized (locker){
            locker. notify();
        }
    }
}

3.5 Thinking

We have added the lock to the entire execution task. If we only lock the wait at this time? Is this thread safe? If it is not safe, give a reason.

Answer: thread insecurity will occur

For example, the following figure explains:

We have two threads at this time. The T1 thread takes out a task at this time (the execution time is 14:30), compares the current time (14:00), and the execution time has not yet arrived. At this time, the task is pushed to the queue. But before pushing, there is a T2 thread at this time, inserting a new task (14:10), and executing the notify operation at the same time, but at this time the T1 thread does not have a wait, so it is empty at this time, at this time T1 The thread starts to get the lock and waits for blocking, but the waiting time at this time is (14:30 – 14:00), and the new task inserted by the T2 thread still needs to be executed in 10 minutes, but because it has been notified once before, it is blocked at this time The wake-up operation cannot be performed at the specified time, so the task inserted by the T2 thread cannot be executed until 14:30, which causes the thread to be unsafe.

When we add a lock to the whole (take out task and push task), the T1 thread must not make the T2 thread execute the notify operation before the wait, because the T1 thread is in the lock, and the lock will be released after waiting for the wait, and the lock of T1 will be released , T2 will execute the notify operation, and then wake up the wait in the T1 thread.