目录
一、定时器
二、标准库中的Timer
三、代码实现
四、死锁
一、定时器
代码中的定时器通常是在一定的时间执行对应的代码逻辑
二、标准库中的Timer
public static void main(String[] args){
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("时间到了后执行业务逻辑");
}
},1000);
}
一个timer可以执行多个定时任务,后续添加任务继续调用schedule方法即可
三、代码实现
首先我们定义一个类用于描述任务
// 用于描述任务
class MyTimerTask {
// 执行的任务
private Runnable runnable;
// 什么时间后执行(绝对的时间)
private long time;
/**
*
* @param runnable 任务
* @param time 多少时间后执行
*/
public MyTimerTask(Runnable runnable, long time) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + time;
}
}
然后我们开始定时器的编写,首先我们需要一个数据结构来存储提交的定时任务,这个数据结构需要能够依次取出最先执行的任务且要是线程安全的,首先想到的是优先级队列其次要有阻塞功能就是阻塞队列,然后我们需要定义一个提交任务的方法该方法中可以将提交的任务存入该队列中,然后在构造方法中创建一个扫描线程不断地取出该队列里地任务进行执行。在此之前我们使用优先级队列是存储定时任务的,那么我们可以先给上面的类实现Compareable接口重写compareTo方法
class MyTimer {
// 1. 创建存储定时任务的数据结构
BlockingQueue<MyTimerTask> queue = new PriorityBlockingQueue<>();
// 2. 定义提交定时任务的方法
public void schedule(Runnable runnable,long after) throws InterruptedException {
MyTimerTask task = new MyTimerTask(runnable,after);
queue.put(task);
}
// 3. 构造方法中定义扫描线程
public MyTimer() {
new Thread(()->{
// 3.1 不断的取出数据看是否需要执行
while (true) {
try {
// 3.1.1 拿出最先需要执行的任务判断是否到达执行时间
MyTimerTask task = queue.take();
if (System.currentTimeMillis() >= task.getTime()) {
// 3.1.2 到达时间执行任务
task.getRunnable().run();
} else {
// 3.1.3 没到时间重回队列
queue.put(task);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
这个时候盲等问题就出现了,比如我们提交了一个2小时后才执行的任务,但是按照上述代码则在这2小时的时间里不断地从队列中取出该任务比较后重回队列,那可以使用sleep(2h)这种方法来解决问题吗?答案是不能,使用sleep方法让线程挂起两个小时可以保证2小时后的任务会被执行,但是如果中途有其他更早的任务提交进来,那么这个任务就会错过执行的时间。那让每次sleep的时间短一点呢?答案同理也是不能的。我们可以使用wait方法来实现,wait(2h)然后在提交任务的方法中一但有新的任务提交调用notify唤醒wait即可,如果在这两个小时内没有任务提交,那么该方法还是会在2h后去执行任务。
// 2. 定义提交定时任务的方法
public void schedule(Runnable runnable,long after) throws InterruptedException {
MyTimerTask task = new MyTimerTask(runnable,after);
queue.put(task);
synchronized (this) {
this.notify();
}
}
// 3. 构造方法中定义扫描线程
public MyTimer() {
new Thread(()->{
// 3.1 不断的取出数据看是否需要执行
while (true) {
try {
// 3.1.1 拿出最先需要执行的任务判断是否到达执行时间
MyTimerTask task = queue.take();
if (System.currentTimeMillis() >= task.getTime()) {
// 3.1.2 到达时间执行任务
task.getRunnable().run();
} else {
// 3.1.3 没到时间重回队列
queue.put(task);
// 3.1.4 阻塞
synchronized (this) {
this.wait(task.getTime() - System.currentTimeMillis());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
这样我们就解决了盲等问题,但是还有一个原子性问题,就是如果此时扫描线程刚好取出了最先要执行的任务该任务是在2h后执行,扫描线程在判断是否到达执行时间之前,其他线程调用添加任务的方法加入了一个1h后需要执行的任务且方法执行完notify没有起到任何作用,此时扫描线程判断完后发现还没有到时间于是将任务入队后调用wait方法等待2h或被唤醒,刚好这2h没有其他任务加入,那么之前提交的1h后的任务就会延迟执行。这是由于扫描线程中操作不是原子性的我们需要调整锁的粒度
new Thread(()->{
// 3.1 不断的取出数据看是否需要执行
while (true) {
try {
synchronized (this) {
// 3.1.1 拿出最先需要执行的任务判断是否到达执行时间
MyTimerTask task = queue.take();
if (System.currentTimeMillis() >= task.getTime()) {
// 3.1.2 到达时间执行任务
task.getRunnable().run();
} else {
// 3.1.3 没到时间重回队列
queue.put(task);
// 3.1.4 阻塞
this.wait(task.getTime() - System.currentTimeMillis());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start()
那么notify代码中的锁粒度是否也需要调整呢?
四、死锁
如果我们将schedule方法中锁的粒度也扩大
public void schedule(Runnable runnable,long after) throws InterruptedException {
synchronized (this) {
MyTimerTask task = new MyTimerTask(runnable,after);
queue.put(task);
this.notify();
}
}
这个时候我们进行测试会发现什么也不会执行,发送了死锁。那这是为什么呢?
首先MyTimer实例被创建时扫描线程开始执行当他执行到此处时会因为阻塞队列中还没有元素而阻塞等待
但是锁还是被持有,此时提交任务的代码执行时发现需要先获取到锁,但是锁是被扫描线程持有,于是他需要阻塞等待,但是扫描线程中的take方法也需要执行了提交任务方法中的put才能继续执行,但是执行put方法有需要扫描线程先释放锁,所以发生死锁,这个时候我们需要将schedule方法中锁的粒度修改回去