定时器相当于一个“闹钟”,在日常生活中,我们需要闹钟的辅佐,在代码中,也经常需要“闹钟”机制(网络通信中经常需设定一个超时时间)。
一.定时器的使用
在Java标准库中,也停供了定时器的实现。
Timer类
Timer timer=new Timer();
timer类提供了一个重要的方法schedule()
需填写两个参数,用于安排任务,以及设定定时时间。
使用代码:
public class Demo1 {
public static void main(String[] args) {
Timer timer=new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
},3000);
System.out.println("程序开始运行");
}
}
定时器可以多个一起使用,安排任务的先后顺序取决于所设置的延迟时间
public class Demo1 {
public static void main(String[] args) {
Timer timer=new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello3");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello2");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello1");
}
},1000);
System.out.println("程序开始运行");
}
}
因hello1延迟时间短,所以hello1先打印,其次是hello2,最后hello3
二.定时器的实现
对于定时器来说 ,有以下几个要点:
1.创建类,描述一个要执行的任务是啥(任务的内容,任务的时间)
class MyTimerTask{
private long time;
private Runnable runnable;
public MyTimerTask(Runnable runnable,long delay){
this.runnable=runnable;
this.time=System.currentTimeMillis()+delay;
}
public long getTime(){
return time;
}
public void run(){
runnable.run();
}
}
time表示程序应该执行的时候,等于当前时间戳+延迟等待的时间。
2.管理多个任务,通过一定的数据结构,把多个任务存起来
首先,考虑使用怎样的数据结构储存。
按照我们的设想,应该是执行时间短的先执行,执行时间晚的在后面依次排队。
在学习数据结构时学到过大根堆,小根堆的数据结构,与场景符合。
我们可以使用优先级队列,在队列中放入MyTimerTask类。
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
但无法直接放入MyTimerTask类,需指定放入的方式(根据什么放入)
public int compareTo(MyTimerTask o) {
return (int)(this.time-o.time);
}
通过比较时间,将MyTimerTask类放入队列。
实现schedule方法,将任务放入队列。
public void schedule(Runnable runnable,long delay){
MyTimerTask myTimerTask=new MyTimerTask(runnable,delay);
queue.offer(myTimerTask);
}
3.有专门的线程去执行这些任务
创建线程去取出,执行队列中的任务。
首先,应该判断队列是否为空
如果为空
则需阻塞等待任务被放入队列
如果不为空,则查看任务,判断当前的时间戳有没有超过任务的时间戳。
如果没有超过,那就将任务进行阻塞等待,并设定等待的时间(任务执行的时间-当前时间戳),并在添加任务进队列时唤醒此处的等待。
注:这么做可以让出cpu资源,若是中间有执行时间更短的任务插队进来,可以让出cpu进行调度
如果当前时间>=任务执行的时间,那么就执行任务,并在执行后取出任务
class MyTimer{
private Object lock=new Object();
PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
public MyTimer(){ Thread t=new Thread(()->{
while (true) {
synchronized (lock) {
while (queue.isEmpty()) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
MyTimerTask current=queue.peek();
if(System.currentTimeMillis()>=current.getTime()){
current.run();
queue.poll();
}
else{
try {
lock.wait(current.getTime()-System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
t.start();
}
然后,再考虑线程安全,以上操作中,涉及写和执行的操作,应该加上锁运行。
完整代码如下:
import java.util.PriorityQueue;
class MyTimer{
private Object lock=new Object();
PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
public MyTimer(){ Thread t=new Thread(()->{
while (true) {
synchronized (lock) {
while (queue.isEmpty()) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
MyTimerTask current=queue.peek();
if(System.currentTimeMillis()>=current.getTime()){
current.run();
queue.poll();
}
else{
try {
lock.wait(current.getTime()-System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
t.start();
}
public void schedule(Runnable runnable,long delay) {
synchronized (lock) {
MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
queue.offer(myTimerTask);
lock.notify();
}
}
}
class MyTimerTask implements Comparable<MyTimerTask>{
private long time;
private Runnable runnable;
public MyTimerTask(Runnable runnable,long delay){
this.runnable=runnable;
this.time=System.currentTimeMillis()+delay;
}
public long getTime(){
return time;
}
public void run(){
runnable.run();
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.time-o.time);
}
}
测试如下:
public static void main(String[] args) {
MyTimer myTimer=new MyTimer();
myTimer.schedule(()->{
System.out.println("hello 3000");
},3000);
myTimer.schedule(()->{
System.out.println("hello 2000");
},2000);
myTimer.schedule(()->{
System.out.println("hello 1000");
},1000);
}
补充
为什么队列不使用PriorityBlockingQueue而是要自己加锁?
使用阻塞队列后,queue.take()操作可能会阻塞,wait()操作也可能会阻塞,此时就有了两把锁。
两把锁,多个线程,容易出现死锁的情况。
需精心控制加锁的顺序,代码编程的复杂的提高。
如果不使用阻塞队列,可以通篇使用一把锁
以上便是全部内容,如有不对,欢迎指正