前言
在Java并发编程学习中,定时器是必不可少的环节。
我们知道线程的调度是随机的,但是有的时候我们就是需要它有序一些,此时的定时器就可以很好的解决这个问题。它可以按照一定的先后顺序,将我们的任务依次执行。
目录
一.Java官方库中的定时器
二. 实现定时器
🎈第一步
🎈🎈第二步
🎈🎈🎈第三步
三.进行测试
一.Java官方库中的定时器
在Java.Util 中,内置了定时器的实现
以下是代码展示:
package Timer; import java.util.Timer; import java.util.TimerTask; public class Demo1 { public static void main(String[] args) { Timer t1 = new Timer(); t1.schedule(new TimerTask() { @Override public void run() { System.out.println("我延迟3000ms执行"); //线程并没有结束,是因为Timer内部的线程阻止了线程结束 } },3000); t1.schedule(new TimerTask() { @Override public void run() { System.out.println("我延迟2000ms执行"); //线程并没有结束,是因为Timer内部的线程阻止了线程结束 } },2000); System.out.println("程序启动"); t1.schedule(new TimerTask() { @Override public void run() { System.out.println("我延迟1000ms执行"); //线程并没有结束,是因为Timer内部的线程阻止了线程结束 } },1000); } }
运行结果:
代码解读:1. 首先使用了Timer类,并且调用了Timer类中的schedule方法
2. schedule方法需要传入两个参数 :需要运行的任务和需要延迟的时间
3.由程序的运行结果可以看出,就算延迟 执行3000ms的语句在最前面,也会被 放到最后来打印。
4.Timer类中其实内置了一个线程扫描器,可以判断出那些线程需要执行。
俗话说的好,自己动手丰衣足食。我们看到了这样的使用方法,可以去深刻理解它内部的实现。那么我们下面自己实现一个简单的定时器代码!
二. 实现定时器
实现定时器的过程其实还是比较复杂的。它牵扯到JavaSE的语法,牵扯到泛型和数据结构中的知识。此处我会在写代码的时候,简单的为大家介绍一下。
首先要明确,我们要实现一个定时器,需要有一下三个必需品:
1. 需要有一个任务类,里面要有 任务 和 延迟的时间。当这个类被实例化的时候,初始化任务和时间。
2.需要有一个数据结构,这个数据结构可以按照我们自己指定的顺序来存放任务。那么很明显,最佳的人选就是 优先级队列。
3.需要有一个扫描线程,这个线程需要不停的扫描队头元素(因为队头元素是时间最近的,最需要执行的),看看是否到达执行的时间。
🎈第一步
首先实现一个任务类。
那么我们可以写出以下代码:
package Timer; import java.util.Date; import java.util.PriorityQueue; class MyTimerTask implements Comparable<MyTimerTask> { //通过这个类,描述了一个任务 //在这里写一个执行的任务 private Runnable runnable; //写一个运行的时间 private long time; //写一个构造方法,当使用MyTimerTask的时候,可以初始化任务和时间 //此处的delay就是 schedule 传入的 相对时间 MyTimerTask(Runnable runnable,long delay) { this.runnable = runnable; this.time = System.currentTimeMillis() + delay; } @Override public int compareTo(MyTimerTask o) { /** * 实现Comparable接口是为了对任务进行排序 */ //让队首元素是最小时间的值 return (int) (this.time - o.time); } public Runnable getRunnable() { return runnable; } public long getTime() { return time; } }
代码解读:
1.我们首先写了一个MyTimerTask任务类,这个类实现了Comparable<>接口,既然实现了接口, 那就要重写里面的 compareTo 方法。这样的作用是为了使得每一个被实例化的任务类,都是可以被比较的。因为我们的任务是有先后执行顺序的!我们重写的compareTo方法,是让队首元素一定是最小时间的任务(迫切需要执行)。
2.这个类里面有 private Runnable runnable; 这个意思是创建了一个Runnable 接口,但是还没有重写里面的run方法。它在等待一个重写了run方法的类~;最好的办法就是在初始化任务的时候,传入这个重写了run方法的类。
3.明白了第二点,那么也就知道MyTimerTask的构造方法为什么要这样写了。但是有一点需要注意的是,初始化的time应该是绝对时间。
绝对时间举例:假设现在是14.00 有个任务我们需要延迟30分钟执行,那么也就是14.30执行。此处的time就是 14.00 + 30 ---> 14.30,System.currentTimeMillis()就是获取当前的系统时间。
🎈🎈第二步
需要有一个优先级队列来按照我们指定的顺序来进行存放任务,这个队列我们可以放到实现扫描线程的类中。当初始化这个类的时候,就相当于初始化好了扫描线程和优先级队列。
那么我们可以写出以下代码:
public class MyTimer { //首先要有一个数据结构,这个数据结构可以将任务排序,将时间最短的排在队头 protected PriorityQueue<MyTimerTask> queue = new PriorityQueue<>(); //使用锁对象,解决线程安全问题 private Object locker = new Object(); public void schedule(Runnable runnable,long delay) { //此处的schedule方法 synchronized (locker) { queue.offer(new MyTimerTask(runnable, delay)); locker.notify(); //此处放完元素 用来唤醒正在等待的线程 } } //此处要写构造方法,当外部调用MyTimer的时候,直接初始化扫描锁 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 (InterruptedException e) { e.printStackTrace(); } } }); t.start(); } }
代码解读:
1.首先我们在这个类中内置了一个优先级队列,这个优先级队列是用来按照我们的规则存放任务的。
2.如何schedule方法有两个参数,一个是需要执行的任务(必须是实现了Runnable接口的子类),一个是需要延迟的时间。
3.在schedule方法中,主要的操作就是往优先级队列中放入元素。我们offer放入元素的时候 new MyTimerTask(runnable, delazy); 意思是在放入元素的时候 初始化MyTimerTask实例
4.我们写的构造方法是最复杂的,构造方法中内置了一个线程 t ,这个线程负责一直扫描队首元素,查看是否到达时间。其实扫描这个操作,就是一直判断队首元素是否到达了时间。
那么我们在队列不为空的时候,需要一直判断(使用while循环)。如果到达了时间,那就执行,并且从任务队列中删除。如果没有到达执行时间,先一直判断。
5.使用加锁机制的原因也很简单:
此处我使用了锁对象的方式,其实锁对象也很简单。就是创建一个对象,这个对象是专门用来判断一段代码中的操作是否被加锁了。
需要注意的是,其实只要实例化好MyTimer这个类,那么wait操作就已经开始执行了。因为此时的队列为空。一旦使用schedule方法,那么这时候有了一个任务,队列不为空。此时就可以解锁了。
🎈🎈🎈第三步
到现在,其实代码已经没有太大的问题了。但是我们仔细一想!如果队列中只有一个任务,假设现在的时间是14.00,我让他30分钟之后执行。那么其实它一直在判断是否到达了14.30,其实根本没这个必要。就好比我定了一个闹钟是14.30叫醒我,那么我一直在看是否到达了14.30,这种行为是很没必要的。这30分钟时可以让线程不做任何操作的,这样可以减少资源开销。 这种现象其实就是忙等现象。
那么我们可以使用wait来解决这个问题:
if (curTime >= task.getTime()) {
//说明当前任务已经到达了时间,开始执行
task.getRunnable().run();
//执行完毕从队列中剔除
queue.poll();
} else {
//此处是如果还没有到任务时间,暂时不执行任务
//此处使用wait的时间版本是为了避免一个线程忙等的情况
locker.wait(task.getTime() - curTime);
}
此时如果还没到执行时间,那么就wait一定的时间。
值得注意的是,schedule方法中的notify有双重作用:
三.进行测试
至此为止,代码已全部写完,下面来测试
package Timer; public class Test { //程序运行,只要这个timer被实例化,此时里面的扫描线程就已经被创建好了 public static void main(String[] args) { //此时在主线程中试用 MyTimer timer = new MyTimer(); timer.schedule(new Runnable() { @Override public void run() { System.out.println("3000执行"); } },3000); timer.schedule(new Runnable() { @Override public void run() { System.out.println("2000执行"); } },2000); timer.schedule(new Runnable() { @Override public void run() { System.out.println("1000执行"); } },1000); } }
运行结果:
是符合我们预想的结果的!
其实代码也挺复杂,执行过程也挺麻烦。可以看下我梳理的流程步骤: