Java Web 实战 09 - 多线程基础之定时器

本篇文章 , 给大家带来的是多线程当中的定时器 , 定时器是一个线程工具,可以用于调度多个定时任务以后台线程的方式运行。
比如说,在开发中,我们需要做一些周期性的操作,比如每隔三分钟就清空一次文件夹,用定闹钟的方式人为的清空肯定是不合适的。这时就可以通过 Timer 和 TimerTask 类来实现,定时完成具体任务的功能。
大家可以跳转到此页面观看效果更加
上一篇文章的链接也给大家贴在这里了
在这里插入图片描述

定时器

1. 官方的定时器

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

public class Demo22 {
    
    
    public static void main(String[] args) {
    
    
        // java.util 里面的一个组件
        Timer timer = new Timer();
        // schedule 这个方法的效果是:"安排一个任务"
        // 第一个参数:new TimerTask() + 重写的 run , 表示任务
        // 第二个参数:多少毫秒之后开始执行任务
        timer.schedule(new TimerTask() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("这是一个要执行的任务");
            }
        },3000);
    }
}

image.png
那这是怎么回事呢 ?
实现定时器 , 背后涉及到多线程 !
Timer 里面有线程 , 这个线程的运行阻止了进程的退出

那为什么不使用 sleep 呢 ?
使用 sleep 是把当前线程给阻塞了 .
sleep 的时间里 , 你啥都干不了 , 就只能干等
但是使用定时器 , 之前的线程该干啥干啥

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

public class Demo22 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        // java.util 里面的一个组件
        Timer timer = new Timer();
        // schedule 这个方法的效果是:"安排一个任务"
        // 第一个参数:new TimerTask() + 重写的 run , 表示任务
        // 第二个参数:多少毫秒之后开始运行
        timer.schedule(new TimerTask() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("这是一个要执行的任务");
            }
        },3000);

        while(true) {
    
    
            System.out.println("其他线程该干啥干啥");
            Thread.sleep(1000);
        }
    }
}

image.png

2. 自己实现的计时器

Timer 其实可以往里面加入很多任务的
我们要实现的计时器 , 就需要考虑下面几个因素

  • Timer内部要组织很多的任务
  • Timer里的每个任务都要通过一定的方式来描述出来 (自己定义一个 TimerTask 类)
  • 还需要有一个线程 , 通过这个线程来扫描定时器内部的任务 , 执行其中时间到了的任务

虽然这里的任务有很多 , 但是他们的执行顺序是一定的 , 按照时间顺序先后来执行 , 所以我们可以使用优先级队列来组织这些任务 , 时间小的 (time 属性小的) 先执行
但是使用优先级队列还有可能出现问题 :
这个队列会被多个线程同时访问

  1. schedule 可能是在多线程环境中被调用 , 每次调用都要往队列中添加元素
  2. 内部还需要有专门的线程来执行队列里的任务

读和写同步进行 , 就可能会造成线程不安全问题
所以我们选择使用 PriorityBlockingQueue
他既满足了优先级队列的特点 : 先进先出 , 又满足了阻塞队列的特点 : 线程安全

2.1 使用优先级阻塞队列

import java.util.concurrent.PriorityBlockingQueue;

// 描述任务的类
class MyTask {
    
    
    // 任务要干啥
    private Runnable command;
    // 任务什么时候执行
    private long time;

    public MyTask(Runnable command,long after) {
    
    
        this.command = command;
        // 任务中需要记录一个完整的时间
        // 记录到底什么时候(几分几秒)执行
        // 比如 2023-01-30 09:24
        this.time = System.currentTimeMillis() + after;
    }

    // 执行任务的方法,直接在内部调用 command 的 run 方法
    public void run() {
    
    
        command.run();
    }
}

// 自己创建的定时器类
class MyTimer {
    
    
    // 使用优先级队列来保存若干个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    // command 代表要执行的任务
    // after 代表多长时间之后来执行这个任务
    public void schedule(Runnable command,long after) {
    
    
        // 创建任务
        MyTask myTask = new MyTask(command, after);
        // 把任务添加到优先级阻塞队列中
        queue.put(myTask);
    }
}

public class Demo23 {
    
    
}

接下来 , 我们就需要安排线程来执行队列中的任务 . 尤其是每次出队列 , 就要执行时间最短的任务

import java.util.concurrent.PriorityBlockingQueue;

// 描述任务的类
class MyTask {
    
    
    // 任务要干啥
    private Runnable command;
    // 任务什么时候执行
    // 设置成 public 方便下面访问
    // 提供他的 public 的 get 方法也可以
    public long time;

    public MyTask(Runnable command,long after) {
    
    
        this.command = command;
        // 任务中需要记录一个完整的时间
        // 记录到底什么时候(几分几秒)执行
        // 比如 2023-01-30 09:24
        this.time = System.currentTimeMillis() + after;
    }

    // 执行任务的方法,直接在内部调用 command 的 run 方法
    public void run() {
    
    
        command.run();
    }
}

// 自己创建的定时器类
class MyTimer {
    
    
    // 使用优先级队列来保存若干个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    // command 代表要执行的任务
    // after 代表多长时间之后来执行这个任务
    public void schedule(Runnable command,long after) {
    
    
        // 创建任务
        MyTask myTask = new MyTask(command, after);
        // 把任务添加到优先级阻塞队列中
        queue.put(myTask);
    }

    public MyTimer() {
    
    
        // 在这里启动一个线程
        Thread t = new Thread(() -> {
    
    
            while(true) {
    
    
                // 循环过程中,不停地从队列中获取到队首元素
                // 我们首先需要判定队首元素当前时间是否就绪;就绪->执行,不就绪->不执行
                // 比如这个任务是 12:30 的 , 现在是 12:15 , 就先不执行
                try {
    
    
                    // 把任务取出来
                    MyTask myTask = queue.take();
                    long curTime = System.currentTimeMillis();
                    if(curTime < myTask.time) {
    
    
                        // 时间还未到
                        // 再把它放回去
                        queue.put(myTask);
                    } else {
    
    
                        // 时间到了
                        // 执行任务
                        myTask.run();
                    }
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });
    }
}

public class Demo23 {
    
    
    public static void main(String[] args) {
    
    
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("任务1");
            }
        },2000);

        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("任务2");
            }
        },4000);

        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("任务3");
            }
        },6000);
    }
}

我们运行一下
image.png
这是因为我们没定义优先级的比较规则

2.2 定义比较规则

import java.util.concurrent.PriorityBlockingQueue;

// 描述任务的类
class MyTask implements Comparable<MyTask> {
    
    
    // 任务要干啥
    private Runnable command;
    // 任务什么时候执行
    // 设置成 public 方便下面访问
    // 提供他的 public 的 get 方法也可以
    private long time;

    public MyTask(Runnable command,long after) {
    
    
        this.command = command;
        // 任务中需要记录一个完整的时间
        // 记录到底什么时候(几分几秒)执行
        // 比如 2023-01-30 09:24
        this.time = System.currentTimeMillis() + after;
    }

    // 执行任务的方法,直接在内部调用 command 的 run 方法
    public void run() {
    
    
        command.run();
    }

    public long getTime() {
    
    
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
    
    
        // 希望时间小的在前面
        // 试一下就知道了
        // this.time - o.time 得到的是 long,返回值是int
        // 所以需要强转
        return (int) (this.time - o.time);
    }
}

// 自己创建的定时器类
class MyTimer {
    
    
    // 使用优先级队列来保存若干个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    // command 代表要执行的任务
    // after 代表多长时间之后来执行这个任务
    public void schedule(Runnable command,long after) {
    
    
        // 创建任务
        MyTask myTask = new MyTask(command, after);
        // 把任务添加到优先级阻塞队列中
        queue.put(myTask);
    }

    public MyTimer() {
    
    
        // 在这里启动一个线程
        Thread t = new Thread(() -> {
    
    
            while(true) {
    
    
                // 循环过程中,不停地从队列中获取到队首元素
                // 我们首先需要判定队首元素当前时间是否就绪;就绪->执行,不就绪->不执行
                // 比如这个任务是 12:30 的 , 现在是 12:15 , 就先不执行
                try {
    
    
                    // 把任务取出来
                    MyTask myTask = queue.take();
                    long curTime = System.currentTimeMillis();
                    if(curTime < myTask.getTime()) {
    
    
                        // 时间还未到
                        // 再把它放回去
                        queue.put(myTask);
                    } else {
    
    
                        // 时间到了
                        // 执行任务
                        myTask.run();
                    }
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

public class Demo23 {
    
    
    public static void main(String[] args) {
    
    
        MyTimer myTimer = new MyTimer();

        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("任务2");
            }
        },4000);

        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("任务1");
            }
        },2000);
        
        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("任务3");
            }
        },6000);
    }
}

但是我们的代码 , 还存在可以优化的地方

2.3 解决 CPU 空转 : wait

image.png

import java.util.concurrent.PriorityBlockingQueue;

// 描述任务的类
class MyTask implements Comparable<MyTask> {
    
    
    // 任务要干啥
    private Runnable command;
    // 任务什么时候执行
    // 设置成 public 方便下面访问
    // 提供他的 public 的 get 方法也可以
    private long time;

    public MyTask(Runnable command, long after) {
    
    
        this.command = command;
        // 任务中需要记录一个完整的时间
        // 记录到底什么时候(几分几秒)执行
        // 比如 2023-01-30 09:24
        this.time = System.currentTimeMillis() + after;
    }

    // 执行任务的方法,直接在内部调用 command 的 run 方法
    public void run() {
    
    
        command.run();
    }

    public long getTime() {
    
    
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
    
    
        // 希望时间小的在前面
        // 试一下就知道了
        // this.time - o.time 得到的是 long,返回值是int
        // 所以需要强转
        return (int) (this.time - o.time);
    }
}

// 自己创建的定时器类
class MyTimer {
    
    
    // 这个是用来阻塞等待的锁对象
    // 方便下面使用 wait
    private Object locker = new Object();

    // 使用优先级队列来保存若干个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    // command 代表要执行的任务
    // after 代表多长时间之后来执行这个任务
    public void schedule(Runnable command, long after) {
    
    
        // 创建任务
        MyTask myTask = new MyTask(command, after);
        // 把任务添加到优先级阻塞队列中
        queue.put(myTask);
        synchronized (locker) {
    
    
            locker.notify();
        }
    }

    public MyTimer() {
    
    
        // 在这里启动一个线程
        Thread t = new Thread(() -> {
    
    
            while (true) {
    
    
                // 循环过程中,不停地从队列中获取到队首元素
                // 我们首先需要判定队首元素当前时间是否就绪;就绪->执行,不就绪->不执行
                // 比如这个任务是 12:30 的 , 现在是 12:15 , 就先不执行
                try {
    
    
                    // 把任务取出来
                    MyTask myTask = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (curTime < myTask.getTime()) {
    
    
                        // 时间还未到
                        // 再把它放回去
                        queue.put(myTask);
                        synchronized (locker) {
    
    
                            // myTask.getTime() - curTime 任务时间-当前时间=等待时间
                            locker.wait(myTask.getTime() - curTime);
                        }
                    } else {
    
    
                        // 时间到了
                        // 执行任务
                        myTask.run();
                    }
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

public class Demo23 {
    
    
    public static void main(String[] args) {
    
    
        MyTimer myTimer = new MyTimer();

        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("任务2");
            }
        }, 4000);

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

        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("任务3");
            }
        }, 6000);
    }
}

image.png

但是 , 当前代码还是有点问题 …

2.4 防止其他线程插入到入队列和线程等待之间

image.png
出现问题的位置是 put 和 synchronized 之间很有可能被别的线程的任务插入
所以我们把 synchronized 范围扩大 , 把 put 和 wait 打包成一组操作

import java.util.concurrent.PriorityBlockingQueue;

// 描述任务的类
class MyTask implements Comparable<MyTask> {
    
    
    // 任务要干啥
    private Runnable command;
    // 任务什么时候执行
    // 设置成 public 方便下面访问
    // 提供他的 public 的 get 方法也可以
    private long time;

    public MyTask(Runnable command, long after) {
    
    
        this.command = command;
        // 任务中需要记录一个完整的时间
        // 记录到底什么时候(几分几秒)执行
        // 比如 2023-01-30 09:24
        this.time = System.currentTimeMillis() + after;
    }

    // 执行任务的方法,直接在内部调用 command 的 run 方法
    public void run() {
    
    
        command.run();
    }

    public long getTime() {
    
    
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
    
    
        // 希望时间小的在前面
        // 试一下就知道了
        // this.time - o.time 得到的是 long,返回值是int
        // 所以需要强转
        return (int) (this.time - o.time);
    }
}

// 自己创建的定时器类
class MyTimer {
    
    
    // 这个是用来阻塞等待的锁对象
    // 方便下面使用 wait
    private Object locker = new Object();

    // 使用优先级队列来保存若干个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    // command 代表要执行的任务
    // after 代表多长时间之后来执行这个任务
    public void schedule(Runnable command, long after) {
    
    
        // 创建任务
        MyTask myTask = new MyTask(command, after);
        
        synchronized (locker) {
    
    
            // 把任务添加到优先级阻塞队列中
            queue.put(myTask);
            locker.notify();
        }
    }

    public MyTimer() {
    
    
        // 在这里启动一个线程
        Thread t = new Thread(() -> {
    
    
            while (true) {
    
    
                // 循环过程中,不停地从队列中获取到队首元素
                // 我们首先需要判定队首元素当前时间是否就绪;就绪->执行,不就绪->不执行
                // 比如这个任务是 12:30 的 , 现在是 12:15 , 就先不执行
                try {
    
    
                    synchronized (locker) {
    
    
                        // 把任务取出来
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (curTime < myTask.getTime()) {
    
    
                            // 时间还未到
                            // 再把它放回去
                            queue.put(myTask);
                            synchronized (locker) {
    
    
                                // myTask.getTime() - curTime 任务时间-当前时间=等待时间
                                locker.wait(myTask.getTime() - curTime);
                            }
                        } else {
    
    
                            // 时间到了
                            // 执行任务
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

public class Demo23 {
    
    
    public static void main(String[] args) {
    
    
        MyTimer myTimer = new MyTimer();

        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("任务2");
            }
        }, 4000);

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

        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("任务3");
            }
        }, 6000);
    }
}

image.png

2.5 队列初识为空的情况下 , 需要阻塞等待

image.png

import java.util.concurrent.PriorityBlockingQueue;

// 描述任务的类
class MyTask implements Comparable<MyTask> {
    
    
    // 任务要干啥
    private Runnable command;
    // 任务什么时候执行
    // 设置成 public 方便下面访问
    // 提供他的 public 的 get 方法也可以
    private long time;

    public MyTask(Runnable command, long after) {
    
    
        this.command = command;
        // 任务中需要记录一个完整的时间
        // 记录到底什么时候(几分几秒)执行
        // 比如 2023-01-30 09:24
        this.time = System.currentTimeMillis() + after;
    }

    // 执行任务的方法,直接在内部调用 command 的 run 方法
    public void run() {
    
    
        command.run();
    }

    public long getTime() {
    
    
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
    
    
        // 希望时间小的在前面
        // 试一下就知道了
        // this.time - o.time 得到的是 long,返回值是int
        // 所以需要强转
        return (int) (this.time - o.time);
    }
}

// 自己创建的定时器类
class MyTimer {
    
    
    // 这个是用来阻塞等待的锁对象
    // 方便下面使用 wait
    private Object locker = new Object();

    // 使用优先级队列来保存若干个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    // command 代表要执行的任务
    // after 代表多长时间之后来执行这个任务
    public void schedule(Runnable command, long after) {
    
    
        // 创建任务
        MyTask myTask = new MyTask(command, after);

        synchronized (locker) {
    
    
            // 把任务添加到优先级阻塞队列中
            queue.put(myTask);
            locker.notify();
        }
    }

    public MyTimer() {
    
    
        // 在这里启动一个线程
        Thread t = new Thread(() -> {
    
    
            while (true) {
    
    
                // 循环过程中,不停地从队列中获取到队首元素
                // 我们首先需要判定队首元素当前时间是否就绪;就绪->执行,不就绪->不执行
                // 比如这个任务是 12:30 的 , 现在是 12:15 , 就先不执行
                try {
    
    
                    synchronized (locker) {
    
    
                        while (queue.isEmpty()) {
    
    
                            locker.wait();
                        }
                        // 把任务取出来
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (curTime < myTask.getTime()) {
    
    
                            // 时间还未到
                            // 再把它放回去
                            queue.put(myTask);
                            synchronized (locker) {
    
    
                                // myTask.getTime() - curTime 任务时间-当前时间=等待时间
                                locker.wait(myTask.getTime() - curTime);
                            }
                        } else {
    
    
                            // 时间到了
                            // 执行任务
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

public class Demo23 {
    
    
    public static void main(String[] args) {
    
    
        MyTimer myTimer = new MyTimer();

        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("任务2");
            }
        }, 4000);

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

        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("任务3");
            }
        }, 6000);
    }
}

到此为止 , 多线程当中的定时器就给大家讲解完毕了
如果对你有收获的话 , 请一键三连~
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_53117341/article/details/129491789