目录
前言
本篇博客主要介绍Java中多线程使用案例之定时器的使用以及如何模拟实现。
一、什么是定时器
定时器也是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后,
就执行某个指定好的代码。
目的:定时器的目的即为了在某个时间点,程序自身主动触发某个事件,而不需要外力去开启或者启动,以节省人力并统一管理。
二、Java标准库提供的定时器的使用
标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule 。
schedule 包含两个参数: 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行。
Timer类的使用:
import java.util.Timer;
import java.util.TimerTask;
public class ThreadDemo22 {
public static void main(String[] args) {
Timer timer = new Timer();
//接下去创多个任务,每相隔1s让定时器执行一个任务
timer.schedule(new TimerTask() {//TimeTask本质上就是Runnable
@Override
public void run() {
System.out.println("hello1");
}
},1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello2");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello3");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello4");
}
},4000);
System.out.println("hello main");
}
}
运行结果:
代码解释:以上代码我们创建了Timer类的实例化对象,通过schedule方法去创建了4个任务,让这四个任务每隔1s执行一个任务。而且我们这个代码在任务都执行完了之后,程序并没有结束。这是因为在Timer内部有一个内置线程,这个内置线程是一个前台线程,所以这里代码并不会因任务结束而结束,不结束是为了确保后面线程继续往定时器中添加任务时可以成功添加。
schedule方法中的TimerTask:TimerTask本质上就是实现了Runnable接口。因此这里要重写run()方法。run()方法当中就是要做的任务。
三、定时器的模拟实现
整体思路:定时器本质上就是类内部提供一个schedule方法,通过schedule方法实现创建多个任务,并放入到队列中,之后再创建一个线程来依次执行这些任务。
所以据上述内容来说:我们需要有一个线程安全的优先级队列,这里我们使用PriorityBlockingQueue,接着还需要有一个任务类(Task),里面包含一个任务和任务执行时间,接着创建一个线程用来执行队列里面的这些任务,最后再创建一个MyTimer类来将任务类和线程包含在一起就可以了
代码实现:
import java.util.concurrent.PriorityBlockingQueue;
class Task implements Comparable<Task>{//用来表示每个任务
//每个任务包括任务等待时间和执行的任务内容,我们通过构造方法来对时间和任务初始化
public long time;
public Runnable runnable;
public Task(Runnable runnable,long delay){
this.runnable = runnable;
this.time = delay+System.currentTimeMillis();//时间以绝对时间戳来表示
}
@Override
public int compareTo(Task o) {
return (int)(this.time - o.time);
}
}
class MyTimer{
private Object locker = new Object();
//内部要有一个线程安全的优先级队列
PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
//实现schedule方法
public void schedule(Runnable runnable,long delay){
Task task = new Task(runnable,delay);//构造出一个新任务
queue.put(task);//任务入队列
synchronized (locker){
locker.notify();//只要有元素入队列就要唤醒
}
}
public MyTimer() throws InterruptedException {//构造方法内部构造线程,并启动线程。
Thread t = new Thread(()->{
Task task = null;
try {
synchronized (locker) {
while (true) {
while (queue.isEmpty()){//队列为空就先阻塞等待
locker.wait();
}
task = queue.take();
if (System.currentTimeMillis() >= task.time) {//如果时间到了或者超过就执行
task.runnable.run();//执行任务
} else {//时间还没到就把任务再放回队列,接着就阻塞等待
queue.put(task);
locker.wait(task.time - System.currentTimeMillis());//阻塞等待,等待有新元素入队列再唤醒
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
}
}
关于代码的几点注意事项:
对于Task类的说明:
对于MyTimer构造方法的说明:
对于以下两个加锁位置的区别:
以下的加锁位置能保证程序正确执行的是上面的那段代码:
原因分析:这里主要原因是因为线程的调度是无序随机的,我们试想一下,假设现在时间是14:00,我们有两个线程t1和t2,并且这时候的队头任务执行时间是14:30;假设t1执行wait之前,cpu调度去执行t2了,而t2是在执行schedule方法。
而对于下面的第一段代码,如果t1也是在执行到wait之前被切走,去执行t2线程,这时候虽然新任务还是被创建出来了,但是由于t1线程还占着锁,所以t2线程的notify就无法执行,会等到t1线程释放了锁才能执行notify操作,这时候就可以正常执行到队头任务了,notify也就不会出现无效执行了。
对于下面的第二段代码:当schedule执行完之后会插入一个新任务(插入的新任务的执行时间是14:10,)并执行notify,这时候t1还没进入wait,这次notify其实就相当于是无效的了。如果这时候,进程调度回t1了,就会继续往下执行wait操作,这时候wait的时间还是30分钟了,这时候就无法及时执行到新的队头任务了。