定时器相当于是一个闹“闹钟”
在代码中,也经常需要“闹钟”机制
网络通信中,经常需要设定一个“超时时间”
方法 | 作用 | |
---|---|---|
void schedule(TimerTask task, long delay) | 指定delay时间之后(单位毫秒)执行任务task |
使用Java标准库中的Timer,在 3s
后打印“hello
”:
import java.util.Timer;
import java.util.TimerTask;
public class Demo4 {
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("程序开始执行");
}
}
还可以制定多个任务:
import java.util.Timer;
import java.util.TimerTask;
public class Demo4 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@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);
System.out.println("程序开始执行");
}
}
对于定时器来说
schedule
的时候,指定的时间是“delay
”值,但是,描述任务的时候,不建议使用 delay
来表示,最好使用“绝对时间”(时间戳)来表示//定时器的任务
class MyTimerTask {
//描述任务是什么
private Runnable runnable;
//通过毫秒时间戳,表示这个任务具体啥时候执行
private long time;
public MyTimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
//获取当前时间戳 + delay,得到一个绝对的时间戳
this.time = System.currentTimeMillis() + delay;
}
public void run() {
runnable.run();
}
public long getTime() {
return time;
}
}>
记录的时间戳,那时间就到了,若 <
,时间就还没到delay
的话,就不好对比了
- 这样就没有了参照物,没有了比较对象
- 比如说你写了一个 delay=5000
,但你不知道什么时候是这个时间(刻舟求剑)12:00
,设置的 delay
为 1 h
,那么此时的 time
就为 13:00
此时我们最先需要做的就是确定是用什么数据结构来存放多任务
List
中删掉因为使用的是优先级队列,所以要指定出比较规则,才能排出优先级
MyTimeTask
类中实现 Comparable
接口class MyTimerTask implements Comparable<MyTimerTask>{
...
...
...
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.time - o.time);
}
}
class MyTimer {
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
}comparaTo
里作差的顺序就决定了是大堆还是小堆
- 这里我们需要的是小堆此时,各元素可以被顺利地添加到这个优先级队列中了,各个任务已经可以被我们用优先级队列管理起来了
之后我们就需要考虑得有人去执行这里面的任务
class MyTimer {
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
public MyTimer() {
//创建线程,负责执行上述队列中的内容
Thread t = new Thread(() -> {
//我们也不知里面有多少个元素,就需要不停地循环去取
while(true){
//在队列不为空的情况下,取出对首元素
if(queue.isEmpty()){
continue;
}
MyTimerTask current = queue.peek();
if(System.currentTimeMillis() >= current.getTime()){
//执行任务,调用 runnable 里面的 run,真正的开始执行任务无逻辑
current.run();
//把执行过的任务,从队列中删除
queue.poll();
}else {
//不执行任务
continue;
}
}
});
t.start();
}
}
System.currentTimeMillis() >= current.getTime()
10:30
,任务时间是 12:00
,不应该执行
- 直接跳过这次循环undefined10:30
,任务时间是 10:29
,就应该执行
- 先执行 runnable
中的 run
方法,随后使用 poll
将这个元素从队列中删去之后我们就需要给定时器里面安排任务,实现 schedule
方法
class MyTimer {
...
...
...
public void schedule(Runnable runnable, long delay) {
MyTimerTask myTimerTask = new MyTimerTask(runnable,delay);
queue.offer(myTimerTask);
}
}
MyTimeTask
对象,参数为所要执行的具体任务和时间,随后将其添加到队列中class MyTimer {
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
public MyTimer() {
//创建线程,负责执行上述队列中的内容
Thread t = new Thread(() -> {
//我们也不知里面有多少个元素,就需要不停地循环去取
while(true){
//在队列不为空的情况下,取出对首元素
if(queue.isEmpty()){
continue;
}
MyTimerTask current = queue.peek();
if(System.currentTimeMillis() >= current.getTime()){
//执行任务,调用 runnable 里面的 run,真正的开始执行任务无逻辑
current.run();
//把执行过的任务,从队列中删除
queue.poll();
}else {
//不执行任务
continue;
}
}
});
t.start();
}
}
对于此处的 while
循环
continue
操作改为 wait/notify
操作。在空的时候 wait
,在不空的时候 notify
(schedule
之后)wait
,避免无意义的循环,直到进行 schedule
操作之后,将其 notify
while
里面的判空if
改为 while
更加安全wait
被一些其他的方式唤醒了,结果队列还是为空,往下走进行 peek
操作,就会出现问题while
后,即使被意外唤醒了,也能够继续确认,是不是要继续 wait
10:45
,任务时间是 12:00
- 这样,线程也会一直循环执行,检查时间到没到:没到就 continue
,然后继续进行循环判定
- 类似于:我定了一个 12:00
的闹钟,此时我一看是 10:45
,我继续睡一会,刚一闭眼,又睁眼看时间,为 10:45
,我又闭眼;刚一闭眼,我又睁眼看时间......
- 这种情况下,并没有完成什么实质性的工作,但还要一直进行循环,也类似于“线程饿死”
- 所以,此时我们也将这里的 continue
改为 wait
,但此时就不需要用 notify
进行唤醒了,我们可以指定一个“超时时间”(当前时间距离任务时间还有多久)
- 注意 :此时不应该使用 sleep
,因为可能存在这样的情况:
1. 你在 sleep
的过程中,新来了一个时间更早的任务,但线程无法被提前唤醒;若使用 wait
的话,每次在 schedule
的时候都会进行 notify
将线程唤醒,让线程再次进行判断,重新设置等待时间,这样就不会错过新的任务
2. sleep
在休眠的时候是不会释放锁的,这样就会造成进行取出和删除操作的线程是抱着锁睡的,之后 schedule
就拿不到锁了,就进行不了新增任务的操作了
我们可以将两个 wait
的异常 try-catch
一起放在外面改后:
class MyTimer {
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
private static Object locker = new Object();
public MyTimer() {
//创建线程,负责执行上述队列中的内容
Thread t = new Thread(() -> {
try {
//我们也不知里面有多少个元素,就需要不停地循环去取
while (true) {
synchronized (locker) {
//在队列不为空的情况下,取出对首元素
while (queue.isEmpty()) {
locker.wait();
}
MyTimerTask current = queue.peek();
if (System.currentTimeMillis() >= current.getTime()) {
//执行任务,调用 runnable 里面的 run,真正的开始执行任务无逻辑
current.run();
//把执行过的任务,从队列中删除
queue.poll();
} else {
//不执行任务
locker.wait(current.getTime() - System.currentTimeMillis());
}
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
}
public void schedule(Runnable runnable, long delay) {
MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
synchronized (locker) {
queue.offer(myTimerTask);
locker.notify();
}
}
}
在定时器里面,我们在这里涉及到的核心是 Queue
这个数据结构,
在这个 Queue
这里,我们有一个专门的线程,从队列里面取元素,删除元素
然后我们还有一个 schedule
方法,去执行插入队列操作
schedule
里面的插入操作是一个线程,取和删操作是另一个线程Queue
,那就肯定是存在线程安全风险的PriorityQueue
,这个类自身不带线程安全的控制能力
所以,此时就一定存在线程安全的风险为了解决这个线程安全问题,我们就给操作都加上锁
class MyTimer {
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
private static Object locker = new Object();
public MyTimer() {
//创建线程,负责执行上述队列中的内容
Thread t = new Thread(() -> {
//我们也不知里面有多少个元素,就需要不停地循环去取
while (true) {
synchronized (locker) {
//在队列不为空的情况下,取出对首元素
if (queue.isEmpty()) continue;
MyTimerTask current = queue.peek();
if (System.currentTimeMillis() >= current.getTime()) {
//执行任务,调用 runnable 里面的 run,真正的开始执行任务无逻辑
current.run();
//把执行过的任务,从队列中删除
queue.poll();
} else {
//不执行任务
continue;
}
}
}
});
t.start();
}
public void schedule(Runnable runnable, long delay) {
MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
synchronized (locker) {
queue.offer(myTimerTask);
}
}
}
主程序:
public class Demo {
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);
System.out.println("程序开始执行");
}
}
//执行结构
程序开始执行
hello 1000
hello 2000
hello 3000
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。