和我们现实中的定时器用途类似,代码中的定时器是设定一个时间,在过去这个时间后,执行某个特定的代码
例如我们的服务器和客户端传递信息,客户端需要等待服务器的信息,如果超出了一定时间还没有收到消息,那么客户端就应该提醒服务器让他重新发一下消息,这里就可以用到计时器
在java.util.Timer包中实现了定时器
首先实现一个Timer对象
Timer timer = new Timer();
然后为计时器布置任务,第一个参数是一个Runnable对象,重写run方法,就可以让定时器到点后执行其中的代码,第二个参数是微秒,
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("时间到");
}
},3000);
我们一个Timer对象,可以安排多个schedule任务
而当我们运行这个代码时,发现程序并没有在执行完代码后退出,这是因为Timer中存在线程来完成任务,之前讲过线程分为前台线程和后台线程(isDaemon判断)而我们的前台线程不会让进程退出
(最后有完整代码,前面的代码只是用来讲解而拆分的)
首先我们实现MyTask,代表计时器中的任务对象
class MyTask implements Comparable<MyTask>{
private Runnable runnable;
private long time;
MyTask(Runnable runnable, long delay){
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
public void run(){
runnable.run();
}
@Override
public int compareTo(MyTask o) {
return (int) (this.time -o.time);
}
}
MyTask中有两个参数,一个是Runnable的任务,另一个是时间,时间的大小是现在时刻+传入的时间,我们实现了其构造方法,以及一系列get方法,至于为什么要实现compareTo方法,这个后面会提到
接下来实现MyTimer
class MyTimer{
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
private Object locker = new Object();
public void schedule(Runnable runnable, long after){
MyTask myTask = new MyTask(runnable, after);
queue.offer(myTask);
synchronized (locker){
locker.notify();
}
}
public MyTimer(){
Thread t = new Thread(() -> {
while(true){
try {
synchronized (locker){
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if(myTask.getTime() > curTime){
queue.put(myTask);
locker.wait(myTask.getTime() - curTime);
} else {
myTask.run();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
t.start();
}
}
由于我们需要判断哪个任务先执行,哪个任务后执行,并且如果没有任务了就等待任务,任务满了就不让继续放任务,因此我们创建了一个优先级阻塞队列(详细概念在上一篇博客中有提到),这也是为什么MyTask要实现compareTo的原因
我们在其中添加了schedule方法,在调用这个方法后就可以new一个MyTask任务,并将其放入阻塞队列
在MyTimer的构造方法中,我们创建了一个线程,其主要功能就是循环判断是否有任务需要执行。先从阻塞队列中取出时间最小的任务,然后将其与现在时间进行比较,如果一样了就执行任务,不一样就放回到队列中。
由于我们不想让线程一直循环执行检测时间的任务,这样太浪费资源了,因此我们思考能不能让线程停一停,首先考虑让线程sleep一个现在时间和任务要执行时间的差值,但是这样有一个问题:如果在这段时间中突然又新增了一个任务,而且这个任务和目前时间的差值比上一个的还小,那么我们就会错过这个任务
因此,我们不能用sleep,那么综合之前几篇博客所讲的,我们可以用wait和motify:在我们线程扫描的时候,wait(当前时间和任务执行时间差值),当我们添加任务的时候,就notify一下睡着了的线程
这时我们还需要考虑synchronized应该圈多大范围的代码块
如果我们的代码块只圈了wait这一条语句,那么可能会出现如下问题
当我们的线程a刚从阻塞队列中take出一个任务,这时另一个线程b就调用了put,插入了一个新的任务,并且这个任务的时间比第一个任务的时间还短,然后线程b一直执行到notify语句,线程a才继续执行,这时线程a还是以第一个任务来计算wait时间的,也就是说我们的线程b的notify并没有对线程a起到作用。
这个问题的出现是因为take和wait计算时间并不是原子性的,从而使线程b有机会插入到其中,因此我们的wait的synchronized代码块应该从take一直圈到wait
那么我们就想到,是不是只要synchronized代码块圈的越大,代码写的就越对呢,事实上并非如此,我们的notify代码如果和queue.offer()方法被圈到了一起,就会出现死锁问题,这是因为我们的queue是一个阻塞队列,其put的实现也是带有synchronized,但是我们put的synchrozed传入的对象和notify使用的对象是不一样的,这样的话代码就会一直阻塞在put
因此我们可以发现,线程是非常麻烦而且容易出错的,这也是为什么其他编程语言尝试简化多线程
例如erlang的actor模型,go的CSP,python的await/async
但是在java和cpp中,多线程是最基本的编程方式
import java.util.concurrent.PriorityBlockingQueue;
class MyTimer{
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
private Object locker = new Object();
public void schedule(Runnable runnable, long after){
MyTask myTask = new MyTask(runnable, after);
queue.offer(myTask);
synchronized (locker){
locker.notify();
}
}
public MyTimer(){
Thread t = new Thread(() -> {
while(true){
try {
synchronized (locker){
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if(myTask.getTime() > curTime){
queue.put(myTask);
locker.wait(myTask.getTime() - curTime);
} else {
myTask.run();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
t.start();
}
}
class MyTask implements Comparable<MyTask>{
private Runnable runnable;
private long time;
MyTask(Runnable runnable, long delay){
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
public void run(){
runnable.run();
}
@Override
public int compareTo(MyTask o) {
return (int) (this.time -o.time);
}
}
public class demo {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("时间到");
}
},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("时间到3a");
}
},5000);
}
}