简述:
计时器用以在用户的规定时间内执行一次用户的动作;
计时器的功能决定其应用场景较为广泛。无论是生活中在对微波炉使用时的定时,还是在编程中对数据的定时检测,亦或者在我们玩游戏时排行榜的定时刷新都有有所使用。
基本思路及问题:
整体思路:创建两个线程,线程一主要负责计时工作,线程二主要负责时间到了后处理方法;先启动第二个线程,然后立即将其阻塞掉,等第一个线程的计时时间到了后唤醒第二个线程,如此往复达到计时功能。
可能遇到的问题:
1.可不可以只用一个线程完成计时和执行方法的工作?
不可以,因为当计时完毕后等待方法执行完才会进行第二次计时,但是执行方法还会花费时间,这样就会导致计时器的计时不准确;故我们要使用两个线程,各司其职。
2.是否应该增加锁使得两个线程之间的执行被规范?
是的,以下面的代码为例,其一:要使用wait方法就必须得要基于锁才能使用;其二:我们将线程二的阻塞加上锁,因为就绪态中有两个线程,若线程二进入cpu后还未来得及将自己阻塞,时间片段就到了,这时他返回就绪态与第一个线程竞争cpu,若第一个线程竞争成功进入cpu并上锁,成功计时并执行唤醒方法后时间片段到了,然后回到就绪态继续竞争cpu。此时而线程二竞争成功了进去后把自己阻塞了,那就得在等下次线程一执行完计时后被唤醒,由此产生计时器的工作失误。加了锁后即使没来得及执行线程二的阻塞方法,线程一也会因为锁子锁上了无法进入而进入阻塞态等待线程二执行完阻塞后将其唤醒。
3.用户的方法是通过接口传递过来的,且不能放在线程二的锁域里,不然就会在线程二被唤醒后执行完dosome方法才开锁,这就又会变成和一个线程工作的效果相同了,顺序执行且计时不准,
源代码及其解释:
package com.mec.clock.core;
public class PrepareTime implements Runnable{
//限定的时间,默认1S,可由用户修改
private int delaytime = 1000;
//保证线程持续工作的标记,加上volatile避免寄存器优化
public volatile boolean goon ;
//锁对象
private Object lock;
//Dealthing是个接口,用于传递定时完成后执行由用户规定的方法
private Dealthing dealthing;
public PrepareTime(int delaytime) {
this(delaytime, null);
}
public PrepareTime(int delaytime,Dealthing dealthing) {
this.lock = new Object();
this.dealthing = dealthing;
this.delaytime = delaytime;
}
public int getDelaytime() {
return delaytime;
}
public void setDelaytime(int delaytime) {
this.delaytime = delaytime;
}
//启动线程
public void startup() {
if(goon == true) {//以启动过无需再次启动
return;
}
if(dealthing == null) {//没有接口对象,及没有要执行的方法
System.out.println("未定义要执行的方法");
return;
}
goon = true;
new Thread(new TimerWork(), "work").start();
new Thread(this, "clcok").start();
}
public void shutdown() {
if(goon == false) {
System.out.println("已停止计时器,无需再次停止");
}
goon = false;
}
@Override
public void run() {
while(goon) {
synchronized(lock) {
try {
//在定时完成后唤醒另一个线程
lock.wait(delaytime);
lock.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class TimerWork implements Runnable{//内部类
public TimerWork() {
}
@Override
public void run() {//执行方法的线程
while(goon) {
synchronized(lock) {
try {//先将自己阻塞起来,等待定时器时间到了后唤醒
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
dealthing.dosomething();//执行用户的方法,一定放到锁域外,不然就会导致锁子无法打开,第一个线程无法执行
}
};
}
}
特殊情况:线程一执行完后唤醒线程二,但是有可能dosome方法极其耗费时间,甚至耗费的时间比计时器的限定时间都长,那就会导致线程一执行完后并没有唤醒任何东西,直到线程二的上一次dosome方法执行完后的下一次线程一唤醒才会继续进行新的方法,所以我们的dosome方法执行时间间隔一定为限定时间的整数倍!