目录
一、认识定时器
1.1 定时器是什么
在软件开发的过程中,定时器是一个重要的组件,类似于一个闹钟一般:达到了设定的时间之后,就执行某个指定好的代码。
定时器的运用场景
比如:
- 网络通信中,客户端请求服务器响应时,如果对方500ms没有返回数据,则断开连接尝试重连。
- 在Map中,希望里面的某个key在4s后过期(自动删除)。
二、Java标准库中的定时器——Timer
这里我们重点介绍Timer类中的schedule方法,schedule方法中有两个参数:第一个参数指定即将要运行的任务代码(通过观察源码得知TimerTask是一个抽象类继续于Runnable接口,这里我们通过重写run方法来指定要执行的任务),第二个参数指定多长时间后执行。
public abstract class TimerTask implements Runnable //jdk源码
代码演示:
import java.util.Timer;
import java.util.TimerTask;
public class Demo23 {
public static void main(String[] args) {
Timer timer = new Timer();
System.out.println("开始计时");
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("时间到1");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("时间到2");
}
},4000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("时间到3");
}
},5000);
}
}
运行结果:
代码分析:
通过观察我们发现即使执行完所有任务后,进程依旧没有退出,这时因为Timer内部需要一组线程来执行注册的任务,而这里的线程被称之为前台进程,会影响进程的退出。
三、定时器的简单实现
步骤一、创建MyTimer类,实现schedule方法。
在Java中Timer类的schedule方法第一个参数是:执行的任务,于是我们通过创建一个类来表示这个任务,任务包含两个方面,一个是具体做什么(使用Runnable接口),一个是什么时候去做(记录一个绝对时间,因为需要使用优先级队列来实任务的执行顺序)。
//用这个类表示任务
class Task {
//要执行的任务
private Runnable runnable;
//什么时候执行任务(是一个时间戳)
private long time;
public Task(Runnable runnable,long delay) {
this.runnable = runnable;
time = System.currentTimeMillis() + delay;
}
}
class MyTimer{
public void schedule(Runnable runnable,long after) {
Task task = new Task(runnable,after);
}
}
public class Demo19 {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("执行任务1");
}
},3000);
}
}
步骤二、让MyTimer这个类实现管理多个任务
考虑到线程安全问题,以及时间复杂度,这里我们使用BlockingQueue来实现。
private BlockingQueue<Task> queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable,long after) throws InterruptedException {
Task task = new Task(runnable,after);
queue.put(task);
}
步骤三、这时任务已经被安排到优先级阻塞队列中了
紧接的,实现一个单独的扫描线程,让这个线程不停的检查队首元素,观察时间是否到了,如果到了,就执行该任务。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
//用这个类表示任务
class Task implements Comparable<Task>{
//要执行的任务
private Runnable runnable;
//什么时候执行任务(是一个时间戳)
private long time;
public Task(Runnable runnable,long delay) {
this.runnable = runnable;
time = System.currentTimeMillis() + delay;
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
@Override
public int compareTo(Task o) {
return (int) (this.time - o.time);
}
}
class MyTimer{
private BlockingQueue<Task> queue = new PriorityBlockingQueue<>();
public MyTimer() {
Thread t = new Thread(() -> {
while(true) {
//取出队首元素
try {
//需要注意的是,由于阻塞队列无法阻塞的取队首元素,
//所以得先将元素取出来才可进行判断。
Task task = queue.take();
if (task.getTime() <= System.currentTimeMillis()) {
//到点了执行任务。
task.getRunnable().run();
}else {
//还没到点,就将取出的任务放回去
queue.put(task);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
public void schedule(Runnable runnable,long after) throws InterruptedException {
Task task = new Task(runnable,after);
queue.put(task);
}
}
public class Demo19 {
public static void main(String[] args) throws InterruptedException {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("执行任务1");
}
},3000);
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");
}
},5000);
System.out.println("开始计时");
}
}
分析:需要注意的是,在进行有关于自定义类型比较时候,需要让比较的类实现Comarable接口并重写其中的compareto方法,否则代码会抛出 Comparable异常。
步骤四、对代码进行优化,让CPU更好的进行调度。
那应该对哪进行优化呢?细心观察我们发现,我们创建的用来扫描队首元素的线程是基于while循环实现的。
while循环为什么这里会造成CPU性能的浪费?
假设有以下场景, 给定计时器一个任务A,该任务在二点半开始执行。
而当前时间为12:00:00.那么由于while循环的缘故,大概12:00:01时刻,这个线程就再次扫描队首元素。12:00:02又再次扫描队首元素。
在这个期间,这个线程就一直处于扫描状态,无法被调度去参与其他工作,并且 这样频繁的看时间对整个线程没有任何益处。
于是我们对其进行调整,对其进行加锁:
但是考虑到多线程环境下,其实以上代码是不安全的,例如以下场景:
线程A先安排了14:00的任务(已经放入阻塞队列中),当前时间为12:00.此时又出现了一个13:00的任务。
于是我们需要对扫描线程整个进行加锁:就可以很好的避免上诉情况的出现(避免在线程还未休眠之前将其唤醒)。
既然扫描线程整个加锁了,那schedule能不能进行整个加锁呢?
答案是不行的,如果加锁了话,会产生死锁的情况。
原因:
实现定时器完整代码:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
//用这个类表示任务
class Task implements Comparable<Task>{
//要执行的任务
private Runnable runnable;
//什么时候执行任务(是一个时间戳)
private long time;
public Task(Runnable runnable,long delay) {
this.runnable = runnable;
time = System.currentTimeMillis() + delay;
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
@Override
public int compareTo(Task o) {
return (int) (this.time - o.time);
}
}
class MyTimer{
private BlockingQueue<Task> queue = new PriorityBlockingQueue<>();
private Object locker = new Object();
public MyTimer() {
Thread t = new Thread(() -> {
while(true) {
synchronized (locker) {
//取出队首元素
try {
//需要注意的是,由于阻塞队列无法阻塞的取队首元素,
//所以得先将元素取出来才可进行判断。
Task task = queue.take();
if (task.getTime() <= System.currentTimeMillis()) {
//到点了执行任务。
task.getRunnable().run();
}else {
//还没到点,就将取出的任务放回去
queue.put(task);
//没到点就wait等待
locker.wait(task.getTime() - System.currentTimeMillis());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
}
public void schedule(Runnable runnable,long after) throws InterruptedException {
Task task = new Task(runnable,after);
queue.put(task);
synchronized (locker) {
locker.notify();
}
}
}
public class Demo19 {
public static void main(String[] args) throws InterruptedException {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("执行任务1");
}
},3000);
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");
}
},5000);
System.out.println("开始计时");
}
}