目录
在服务器开放中,客户端向服务器发送请求,等待服务器响应,但若因为某个故障,导致程序一直无响应,怎么办?这时总不可能让客户端一直都没有响应,一直等下去,很有可能程序就卡死了,所以为了应对此种情况,程序员就设置了一个定时器,若到在定时器规定的时间没有完成任务,会执行某一个动作,响应客户端;
标准库中也有定时器(如下图)
![]()
标准库中定时器这个类里有个schedule方法,就是用来安排在多长时间之后来执行安排好的任务,因此他有两个参数(如下图)

一个参数是要执行的任务,就是一个Runnable,需要继承TimerTask,重写run方法,从而指定要执行的任务; 另一个参数是等待时间(单位是毫秒),也即是经过多长时间后,执行任务;
一个定时器中,可以同时安排多个任务,并且执行完任务之后,进程并没有退出,Timer内部需要一组线程来执行任务,这里的线程才是“前台线程”,会影响进程的退出;
如下代码:(安排一个时间表)
- public static void main(String[] args){
- Timer timer = new Timer();
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- System.out.println("12:00 唱歌~");
- }
- }, 2000);
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- System.out.println("12:30 弹钢琴");
- }
- }, 3000);
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- System.out.println("13:00 弹吉他");
- }
- }, 4000);
- System.out.println("开始玩音乐");
- }
执行结果如下:

schedule方法的第一个参数便是任务,所以我们需要能够描述一个任务,怎么描述呢?这里的任务需要包含两个信息,一个是要干什么事(Runnable),另一个是要什么时候执行;(如下图)

分析:
知道了任务和要执行的时间,这样就够了吗?想象一下,刚刚提到,一个定时器可以同时安排多个任务,那么这多个任务必然会涉及到先后发生的问题,那么供大于求的时候也就需要涉及到排序,可是任务是个类,怎么排序呢?比较器——继承Comparable接口,重写compareTo方法;(如下图)

分析:
这里由于time是long类型,所以需要强制类型转换,还有一个问题:“这里的return到底是this.time - o.time 还是 o.time - this.time 呢?”这里最好不要背,比较比较抽象,返回规则不是那么直观,所以,你只需要随便写一个,运行程序试一下,这多试几次,不也就知道吗~
可以用ArrayList吗?刚刚咱提到当供大于求时任务存储需要按序存储,ArrayList显然时无序,不方便找到时间最小的任务,所以这里不难想到使用优先级队列,但由于schedule时有可能在多线程中调用的,并且优先级队列并不是线程安全的,所以可以使用优先级阻塞队列(BlockingQueue)来实现;
从队列中取元素,可以用一个“扫描线程”来不停的检查队首元素,检查时间是否到了,若时间到了,则取出队首元素,执行任务,若时间没到,则将队首元素放回;(如下图)

分析:
由于是阻塞队列,所以因该先取出任务,才能够判断时间是否已经到了,若时间还没到,就把任务继续放回队列
存在问题分析一:“忙等”
观察代码,不难发现当时间还没到的时候,队列会循环的将队首元素取出,然后放回,这个循环就是在忙等,CPU并没有空闲出来,因此这里的等待也就变的毫无意义;
注意这里不可以用sleep(),假设当前时间为9点,任务时间是10点,那么你将设置 sleep 的时间为一个小时,这样合理吗?看似合理,但在多线程的情况下,很有可能会有新的任务出现,若新的任务时间为9点半,那么新的任务就需要执行sleep到10点,那么就会导致新的任务无法在9点半的时候执行!
解决办法:
使用wait()就可以很好的解决上述问题;如下图


存在问题分析二:“notify()空唤醒”
假设当前时间为9点,而当前任务执行的时间为11点,试想如果schedule在t1线程刚执行完take,还没有到wait的时候又新增了一个任务,这个任务的执行时间为10点,这会发生什么?
首先notify会在t1线程还没有wait的时候先唤醒一次,就相当于啥也没做,但其实notify即使什么线程都没唤醒,并不会存在什么问题,但是严重的问题在后面!当扫描线程继续往下执行,发现时间还没到,就将刚刚take出来的任务又放回了阻塞队列,然后就进行了wait等待,这以等待便是2个小时,这意味着,刚才新来的任务需要在10点的时候执行,却被错过了!
解决办法:
刚才出现问题的原因实际上就是因为notify在take和wait之间执行的,现在只需要把扫描线程中的锁范围扩大,保证take和wait之间这段操作的原子性,这样,就可以避免notify在take和wait之间执行;
来分析一下具体过程,9点的时候来了一个任务,需要在11点执行,扫描线程会先拿到锁,然后take,(这时将一个新的任务放入了队列,这个任务的执行时间为10点),接着实现中间逻辑,发现没到时间,就将take出的元素放入队列(此时他已经不是队首元素了),最后到wait;在这个过程中,schedule线程会一直阻塞等待锁,当扫描线程执行了wait,释放了锁,这个时候需要在10点开始执行的任务才拿到锁,执行notify唤醒了正在wait的线程,于是,继续循环取出队首元素,而此时,取出的队首元素便是要在10点执行的那个任务;(如下图)

这样写,就已经基本没问题了;
最后可以思考这样一个问题:若这样写(如下图),会存在什么问题?

问题分析:“死锁”
假设当我new了一个刚刚写好的MyTimer,那么此时就会初始化并调用MyTimer构造方法,构造方法中会创建一个线程t1,并开始线程,那么就会执行run方法,此时线程t1拿到锁,程序进入run方法,接着进行take操作,但是阻塞队列中没有元素,因此阻塞队列就会阻塞等待,直到有任务放入队列中,此时,来了一个任务,通过myTimer.schedule(//任务...,//时间...),此时进入schedule方法,但是由于刚刚线程t1已经拿到锁,schedule也会阻塞等待,所以此时schedule无法将任务put到队列中,这时t1在阻塞等待,schedule也在阻塞等待,就出现了死锁;
所以一定要把put操作放在所外面!
- //任务
- class MyTask implements Comparable
{ - //将要执行的任务
- private Runnable runnable;
- //多久后执行任务
- private long time;
- public MyTask(Runnable runnable, long time){
- this.runnable = runnable;
- this.time = time + System.currentTimeMillis();
- }
- public Runnable getRunnable() {
- return runnable;
- }
- public long getTime() {
- return time;
- }
- public int compareTo(MyTask o) {
- return (int)(this.time - o.time);
- }
- }
- //计时器
- class MyTimer {
- //优先级阻塞队列来存放任务,保证时间最小的先出队
- private BlockingQueue
queue = new PriorityBlockingQueue<>(); - private Object locker = new Object();
- public MyTimer() {
- //创建一个线程,不断来扫描下一个任务
- Thread t1 = new Thread(new Runnable() {
- @Override
- public void run() {
- while(true){
- synchronized(locker) {
- try {
- //取出队首元素
- MyTask task = queue.take();
- //获取当前时间
- long nowTime = System.currentTimeMillis();
- //若到时了就执行任务,若没到时就阻塞等待
- if(nowTime >= task.getTime()){
- task.getRunnable().run();
- }else{
- //先将取出的元素放回
- queue.put(task);
- locker.wait(task.getTime() - nowTime);
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- });
- t1.start();
- }
- //安排
- public void schedule(Runnable runnable, long time) throws InterruptedException {
- queue.put(new MyTask(runnable, time));
- synchronized(locker) {
- locker.notify();
- }
- }
- }
- //测试
- public class Test {
- 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);
- myTimer.schedule(new Runnable() {
- @Override
- public void run() {
- System.out.println("执行任务3");
- }
- },3000);
- System.out.println("开始执行任务");
- }
- }
