[JavaEE] Multi-threading case-timer

Insert image description here

1 Introduction

In life, we must have encountered a request timeout when visiting a website or loading a video. What should we do if the client sends a request to the server but does not get a response? Do you want us to wait to death? This is definitely unrealistic, so we need to use our timer at this time. The client sends a request to the server. If no response is received for a period of time, the timer will decide whether to resend the request or stop the request. In the multi-threading case, the timer is a more complex case, and it is also a more important multi-threading case. Today I will share with you knowledge about timers.
Insert image description here

2. How to use the timer provided by the Java standard library

The timer implementation is provided under Java's util package, and we can directly use the timer provided by the Java standard library.

import java.util.Timer;
import java.util.TimerTask;

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

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

        timer.schedule(new TimerTask() {
    
    
            @Override
            public void run() {
    
    
                System.out.println(1000);
            }
        },1000);
        System.out.println("定时器开始");
    }
}

Insert image description here

  • Timer class represents the timer class
  • Call the schedule method of the Timer class and add the TimerTask class to the timer
  • The TimerTask class includes the specific implementation content of the task and the time to execute the task.

Insert image description here
delay indicates how long it takes to execute this task from the time the schedule method is called. No matter which task is added first, it will always be executed in ascending order of time. And if we observe carefully, we can find that when the contents of the timer are executed, the entire thread does not end. This is because the thread inside the Timer prevents the thread from ending.

3. How to implement a timer yourself

3.1 Build the MyTimerTask task class

Although the Java standard library provides us with a timer, as beginners, we still need to know how it is implemented. Being able to implement a timer ourselves is very important for understanding timers.

class Mytimer {
    
    
    private long time;
    private Runnable runnable;
    public Mytimer(Runnable runnable, long delay) {
    
    
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }
    
    public long getTime() {
    
    
        return this.time;
    }
    
    public Runnable getRunnable() {
    
    
        return this.runnable;
    }
}

First create a MyTimerTask class to describe the specific content of the task, including the operations performed and the execution time.

3.2 Use priority queues to store multiple tasks

After creating a MyTimerTask class to describe specific content, what should be used to store these multiple MyTimerTask tasks? Because it is executed in order of time, tasks with a closer time have a higher priority. In this case, we can use the priority queue data structure to store the MyTimerTask class, and create a small root heap where the task with the closest time is placed. At the top of the pile.

	PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

3.3 Implement schedule method to add tasks

When the schedule method is called, the offer method of PriorityQueue is called to add the task to the priority queue.

	public void schedule(MyTimerTask myTimerTask) {
    
    
        queue.offer(myTimerTask);
    }

3.4 Implement the scanning thread and specific details in the Timer class

A scanning thread is needed in the Timer thread to see whether the task on the top of the heap should be executed. When the priority queue is empty, the thread needs to enter the waiting state and is not awakened until the schedule method is called to add tasks to the queue. So our previous schedule method needs to be slightly adjusted.

	private static Object locker = new Object();

    public void schedule(MyTimerTask myTimerTask) {
    
    
        synchronized (locker) {
    
    
            queue.offer(myTimerTask);
            locker.notify();
        }
    }

    public MyTimer() {
    
    
        Thread t = new Thread(() -> {
    
    
            while (true) {
    
    
                try {
    
    
                    synchronized (locker) {
    
    
                    	//通常线程被唤醒之后的判断不使用if,
                    	//而是使用while循环,因为唤醒线程的方法不止有notify
                    	//interrupt方法也可以唤醒处于等待状态的线程
                    	//当处于等待状态的线程是因为interrupt方法唤醒的时候
                    	//就说明没有执行schedule方法,此时queue中还是为空的
                        while (queue.isEmpty()) {
    
    
                            locker.wait();
                        }
                    }
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        });
    }

The benefit of calling the schedule method here is not only to wake up the waiting state when the heap is empty, but also has some benefits, which we will explain to you when we meet later.

When the heap is not empty, it is necessary to determine whether the task at the top of the heap has reached the time that needs to be executed. If it has reached the time that needs to be executed, it will be executed. If it has not, there is no need to do it.

	public MyTimer() {
    
    
        Thread t = new Thread(() -> {
    
    
            while (true) {
    
    
                try {
    
    
                    synchronized (locker) {
    
    
                        while (queue.isEmpty()) {
    
    
                            locker.wait();
                        }

                        MyTimerTask task = queue.peek();
                        if (curTime >= task.getTime()) {
    
    
                            task.getRunnable().run();
                            queue.poll();
                        }else {
    
    
                            ;
                        }
                    }
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

Let's see what happens when we run this timer

import java.util.PriorityQueue;

class MyTimerTask {
    
    
    private long time;
    private Runnable runnable;
    public MyTimerTask(Runnable runnable, long delay) {
    
    
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }

    public long getTime() {
    
    
        return this.time;
    }

    public Runnable getRunnable() {
    
    
        return this.runnable;
    }
}

class MyTimer {
    
    
    PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    private static Object locker = new Object();

    public void schedule(Runnable runnable, long delay) {
    
    
        synchronized (locker) {
    
    
            queue.offer(new MyTimerTask(runnable,delay));
            locker.notify();
        }
    }

    public MyTimer() {
    
    
        Thread t = new Thread(() -> {
    
    
            while (true) {
    
    
                try {
    
    
                    synchronized (locker) {
    
    
                        while (queue.isEmpty()) {
    
    
                            locker.wait();
                        }

                        MyTimerTask task = queue.peek();
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
    
    
                            task.getRunnable().run();
                            queue.poll();
                        }else {
    
    
                            ;
                        }
                    }
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

public class Demo3 {
    
    
    public static void main(String[] args) {
    
    
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println(3000);
            }
        },3000);

        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println(2000);
            }
        },2000);

        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println(1000);
            }
        },1000);

        System.out.println("计时器开始");
    }
}

Insert image description here

Why is an error reported here?

3.5 Modification and optimization of timer

We have missed a detail here. When we studied the priority queue earlier, we knew that the elements in the priority queue must be comparable elements, the same as TreeMap and TreeSet, but MyTimerTask is a class, and there is a gap between classes. Size comparison is not possible, so we need to override the compareTo method in MyTimerTask to specify the comparison method.

class MyTimerTask implements Comparable<MyTimerTask>{
    
    
    private long time;
    private Runnable runnable;
    public MyTimerTask(Runnable runnable, long delay) {
    
    
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }

    public long getTime() {
    
    
        return this.time;
    }

    public Runnable getRunnable() {
    
    
        return this.runnable;
    }

    @Override
    public int compareTo(MyTimerTask o) {
    
    
        return (int) (this.time - o.time);
    }
}

When such modifications are made, the elements in the heap are comparable. As of writing this, the function has actually been implemented, but what? Optimization also needs to be done: when the top element of the heap has not reached the execution time, is there really nothing to do? If nothing is done, the code will return to the while loop part above and continue to determine whether the top element of the heap has reached the execution time. Because the computer's execution speed is very fast, then before the time for executing the task of the top element of the heap is reached, Tens of thousands of judgments have been made within this period, then this situation is called busy waiting . To solve this busy waiting problem, when judging the execution time of the element at the top of the heap that has not yet been reached, we let the thread wait for a certain period of time and prevent the thread from continuing to judge in a loop.

	public MyTimer() {
    
    
        Thread t = new Thread(() -> {
    
    
            while (true) {
    
    
                try {
    
    
                    synchronized (locker) {
    
    
                        while (queue.isEmpty()) {
    
    
                            locker.wait();
                        }

                        MyTimerTask task = queue.peek();
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
    
    
                            task.getRunnable().run();
                            queue.poll();
                        }else {
    
    
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

At this point we can talk about another benefit of the notify method of the previous schedule method: if a task is added while the Timer is waiting to execute the top element of the heap, and the execution time of this task is earlier than the previous element on the top of the heap The execution time of the task, then when the addition is completed, the waiting state of the Timer will be awakened, and then the newly added task will be placed on the top of the heap, and the task will be executed next.

4. Complete code of timer

import java.util.PriorityQueue;

class MyTimerTask implements Comparable<MyTimerTask>{
    
    
    private long time;
    private Runnable runnable;
    public MyTimerTask(Runnable runnable, long delay) {
    
    
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }

    public long getTime() {
    
    
        return this.time;
    }

    public Runnable getRunnable() {
    
    
        return this.runnable;
    }

    @Override
    public int compareTo(MyTimerTask o) {
    
    
        return (int) (this.time - o.time);
    }
}

class MyTimer {
    
    
    PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    private static Object locker = new Object();

    public void schedule(Runnable runnable, long delay) {
    
    
        synchronized (locker) {
    
    
            queue.offer(new MyTimerTask(runnable,delay));
            locker.notify();
        }
    }

    public MyTimer() {
    
    
        Thread t = new Thread(() -> {
    
    
            while (true) {
    
    
                try {
    
    
                    synchronized (locker) {
    
    
                        while (queue.isEmpty()) {
    
    
                            locker.wait();
                        }

                        MyTimerTask task = queue.peek();
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
    
    
                            task.getRunnable().run();
                            queue.poll();
                        }else {
    
    
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

public class Demo3 {
    
    
    public static void main(String[] args) {
    
    
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println(3000);
            }
        },3000);

        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println(2000);
            }
        },2000);

        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println(1000);
            }
        },1000);

        System.out.println("计时器开始");
    }
}

Insert image description here

Guess you like

Origin blog.csdn.net/m0_73888323/article/details/132994782