文章目录
- 定时器
 - 1 什么是定时器
 - 2 标准库中定时器
 - 3 实现一个定时器
 - 3.1 实现的思路
 - 3.2 为什么要使用优先级队列来保存任务
 - 3.3 开始实现
 
定时器
1 什么是定时器
 定时器 类似于一个 “闹钟”,达到一个设定的时间之后,就执行某个指定好的代码。
定时器是一种实际开发中非常常用的组件。
比如网络通信中,如果对方 500ms 内没有返回数据,则断开连接尝试重连。
 比如一个 Map,希望里面的某个 key 在 3s 之后过期(自动删除)。
类似于这样的场景就需要用到定时器。
2 标准库中定时器
 Timer 这个类就是标准库的定时器
 Timer timer = new Timer();
 
定时器使用
package thread;
import java.util.Timer;
import java.util.TimerTask;
public class ThreadDemo4 {
    public static void main(String[] args) {
        // Timer 这个类就是标准库的定时器
        System.out.println("程序启动!");
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时器任务启动");
            }
        },5000); //5000毫秒后执行 run 方法中的任务
    }
}
 
 
 
 在等待了5000毫秒后,就执行了定时器的任务。
 
 
schedule 这个方法的效果是给定时器注册一个任务。
 但是这个任务不会立即执行,而是在指定时间进行执行。
3 实现一个定时器
3.1 实现的思路
 1、让被注册的任务能够在指定时间被执行
- 单独在定时器内部搞个线程,让这个线程周期性的扫描,判定任务是否到时间了。
 - 如果到时间了,就执行;没到时间就等等。
 
2、一个定时器是可以注册多个任务的,这多个任务会按照约定时间按顺序执行
- 这里的多个任务需要使用 优先级队列 来保存。
 
3.2 为什么要使用优先级队列来保存任务
 定时器里的每一个任务都是带有 “时间” 概念的,也就是多长时间过后就执行。
 可以肯定的是,时间越靠前的越先执行。
可以把时间小的,作为优先级最高。
 此时的队首元素就是整个队列中最要先执行的任务。
此时只需要扫描线程扫描队首元素即可,而不必遍历整个队列。
 因为如果队首元素还没到执行的时间,后续的元素就更不可能到执行的时间。
3.3 开始实现
 1、我们可以使用标准库中带有阻塞功能的优先级队列: PriorityBlockingQueue 来保存
 要执行的任务。
 
 定义一个类来表示我们要执行的任务和执行的时间
//表示定时器中的任务
class MyTask {
    //任务执行的内容
    private Runnable runnable;
    //执行的时间 - 毫秒时间戳表示
    private Long time;
    //构造方法
    public MyTask(Runnable runnable, Long time) {
        this.runnable = runnable;
        this.time = time;
    }
    //获取当前任务的时间
    public Long getTime() {
        return time;
    }
    //执行任务
    public void run() {
        runnable.run();
    }
}
 
 此时 MyTask 就是要保存在 PriorityBlockingQueue 中的任务
class MyTimer {
    //扫描线程
    private Thread search = null;
    //保存任务的阻塞优先级队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
}
 
 2、定时器类需要注册一个 “schedule” 方法来注册任务
 
 我们期望这里保存的是一个 绝对时间,而 after 是一个像 1000ms 这样的毫秒级时间,
 一个时间间隔。
所以需要使用当前的时间戳加上 System.currentTimeMillis() 得到一个是在什么时间去执行的标准时间戳。
//第一个参数是任务内容
//第二个参数是任务在多少毫秒之后执行
public void schedule(Runnable runnable, Long after) {
    //注意这里的时间换算
    MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
    queue.put(task); //填到队列当中
}
 
 3、如何实现扫描线程的主要逻辑
 
 1、因为使用的是 优先级队列,所以这里只要取出队首元素即可。
  MyTask myTask = queue.take();
 
 2、计算出当前的时间
 Long curTime = System.currentTimeMillis();
 
 3、如果到了执行任务的时间就执行,没到就把任务重新塞回队列中
 if (curTime < myTask.getTime()) {
     // 要把任务塞回到队列中
     queue.put(myTask);
 } else { // 到执行任务时间了
     // 执行任务
     myTask.run();
 }
 
 完整代码
 //构造方法里创建一个线程
 public MyTimer() {
     search = new Thread(() -> {
         while (true) {
             try {
                 // 取出队首元素,检查队首元素任务是否到时间了
                 // 如果没到时间,就把任务重新放到队列中
                 // 如果到时间了,就执行任务
                 MyTask myTask = queue.take(); //拿出队首元素
                 long curTime = System.currentTimeMillis(); //计算当前的时间
                 // 还没到执行的时间
                 if (curTime < myTask.getTime()) {
                     // 要把任务塞回到队列中
                     queue.put(myTask);
                 } else { // 到执行任务时间了
                     // 执行任务
                     myTask.run();
                 }
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     });
     search.start();
 }
 
 上述代码存在的两个问题
 
 1、没有指定 MyTask 怎么比较优先级
 
 现在执行两个任务看一下状况
 public static void main(String[] args) throws InterruptedException{
     MyTimer myTimer = new MyTimer();
     myTimer.schedule(new Runnable() {
         @Override
         public void run() {
             System.out.println("任务1");
         }
     }, 1000);
     myTimer.schedule(new Runnable() {
         @Override
         public void run() {
             System.out.println("任务2");
         }
     }, 2000);
 }
 
 
 
 Comparable 用来描述比较规则的接口,这里提示我们还没有描述规则的 Comparable 接口。
 
 可以让 MyTask 类实现 Comparable 接口
 或者也可以使用 Comparable 单独写一 个比较器。
 
 下面是实现一个 Comparable
class MyTask implements Comparable<MyTask> {
    @Override
    public int compareTo(MyTask o) {
        // 这里会返回 <0 >0 =0 三种结果
        // this 比 o 小 返回 <0
        // this 比 o 大 返回 >0
        // this 等于 o 返回 =0
        return (int) (this.time - o.time); // 因为时间是long类型的,所以返回需要强制类型转换
    }
}
 
 
 
 2、如果执行的时间没到就会一直重复取出来塞进去的操作(忙等)
 
 按理说,等待是要释放 CPU 资源的,让 CPU 资源可以干别的事情。
 但是忙等,即进行了等待,又占用着 CPU 资源。
就像是有的人虽然今天休假,但是一会又要线上开会,一会又要打扫卫生。
 自己还没怎么休息,但是一天就过去了,自己虽然是在休假,但是也没有闲着。
 
 如果此时还没到任务执行的时间,比如说任务执行的时间是 14:00, 但是现在是 13:00
 那么在这个时间段内,上述代码的循环操作就可能会被执行数十亿次,甚至更多。
就好比 18:00 就下课了,但是此时是 17:30 ,我过一会看一下时间,过一会看一下时间。
 虽然是在等待着下课时间的到来,但是我也没有闲着。
 
 
 针对上述的情况,不要在忙等了,而是要进行阻塞式等待。
 可以使用 sleep 或者 wait 。
不使用 sleep 的原因:
-  
随时都有可能有新的任务到来,如果新任务执行的时间更早呢。
也就是说这里等待的时间不明确。 -  
如果新的任务执行的时间是 30 分钟后,但是 sleep 设置的时间是 1个小时,
那么这个时候就会错过这个任务。 
使用 wait 更合适,更方便随时唤醒。
 如果有新的任务来了就 notify 唤醒,然后在检查一下时间,重新计算要等待的时间。
 而且 wait 也提供了一个带有 “超时时间” 的版本
带有超时时间的 wait 就可以保证:
- 当新任务来的时候,随时 notify 唤醒
 - 如果没有新任务,则最多等到之前旧任务中的最早任务时间到,就会被唤醒。
 
在 put 操作之后 进行 wait,还要搭配锁来使用。
 
 在 schedule 方法里进行唤醒(notify)
synchronized (this) {
    this.wait(myTask.getTime() - curTime); // 得到需要等待的时间
}
// 唤醒wait
synchronized (this) {
    this.notify();
}
 
 此时的代码还有一个和线程随机调度相关的问题
 
 假设代码执行到了 ** queue.put(myTask);** ,这个线程就要从 cpu 调度走了。
 当线程回来之后,接下来就要进行 wait 操作了,此时 wait 的时间已经是算好的。
比如当前时间是 13:00 ,任务时间是 14:00 ,即将要 wait 1 小时。(此时还没有执行wait)
 如果此时有另一个线程调用了 schedule 方法添加新任务,新任务是 13:30 执行。
由于 扫描线程 wait 还没执行呢,所以此处的 notify 只是会空打一炮,
 不会产生任何的唤醒操作。
此时此刻,新的任务虽然已经插入到队列,新的任务也是在队首,
 紧接着,扫描线程回到 cpu 了,此时等待的时间仍然是 1 小时。
因此,13:30 的任务就被错过了。
 
 
 了解了上述问题之后就不难发现,问题出现的原因,是因为当前 take 操作和 wait 操作不是原子的。
如果在 take 和 wait 之间加上锁,保证在这个过程中不会有新的任务过来,问题自然解决。
 //构造方法里创建一个线程
 public MyTimer() {
     search = new Thread(() -> {
         while (true) {
             try {
                 // 取出队首元素,检查队首元素任务是否到时间了
                 // 如果没到时间,就把任务重新放到队列中
                 // 如果到时间了,就执行任务
                 synchronized (this) {
                     MyTask myTask = queue.take(); //拿出队首元素
                     long curTime = System.currentTimeMillis(); //计算当前的时间
                     // 还没到执行的时间
                     if (curTime < myTask.getTime()) {
                         // 要把任务塞回到队列中
                         queue.put(myTask);
                         // put 之后进行 wait
                         this.wait(myTask.getTime() - curTime); // 得到需要等待的时间
                     } else { // 到执行任务时间了
                         // 执行任务
                         myTask.run();
                     }
                 }
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     });
     search.start();
 }
 
完整代码
package thread;
import java.util.concurrent.PriorityBlockingQueue;
//表示定时器中的任务
class MyTask implements Comparable<MyTask> {
    //任务执行的内容
    private Runnable runnable;
    //执行的时间 - 毫秒时间戳表示
    private long time;
    public MyTask(Runnable runnable, Long time) {
        this.runnable = runnable;
        this.time = time;
    }
    //获取当前任务的时间
    public long getTime() {
        return time;
    }
    //执行任务
    public void run() {
        runnable.run();
    }
    @Override
    public int compareTo(MyTask o) {
        // 这里会返回 <0 >0 =0 三种结果
        // this 比 o 小 返回 <0
        // this 比 o 大 返回 >0
        // this 等于 o 返回 =0
        return (int) (this.time - o.time); // 因为时间是long类型的,所以返回需要强制类型转换
    }
}
class MyTimer {
    //扫描线程
    private Thread search = null;
    //保存任务的阻塞优先级队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    //构造方法里创建一个线程
    public MyTimer() {
        search = new Thread(() -> {
            while (true) {
                try {
                    // 取出队首元素,检查队首元素任务是否到时间了
                    // 如果没到时间,就把任务重新放到队列中
                    // 如果到时间了,就执行任务
                    synchronized (this) {
                        MyTask myTask = queue.take(); //拿出队首元素
                        long curTime = System.currentTimeMillis(); //计算当前的时间
                        // 还没到执行的时间
                        if (curTime < myTask.getTime()) {
                            // 要把任务塞回到队列中
                            queue.put(myTask);
                            // put 之后进行 wait
                            this.wait(myTask.getTime() - curTime); // 得到需要等待的时间
                        } else { // 到执行任务时间了
                            // 执行任务
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        search.start();
    }
    //第一个参数是任务内容
    //第二个参数是任务在多少毫秒之后执行
    public void schedule(Runnable runnable, long after) {
        //注意这里的时间换算
        MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
        queue.put(task); //填到队列当中
        // 唤醒wait
        synchronized (this) {
            this.notify();
        }
    }
}
public class ThreadDemo5 {
    public static void main(String[] args) throws InterruptedException {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1");
            }
        }, 1000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        }, 2000);
    }
}
                

















