需求分析
- 由用户自行设定触发事件
- 由用户提供具体工作过程
- 在规定的触发事件到达时,自动执行具体工作过程。尽可能保证时间的精确性。
这个工具有广泛的用途,例如轮询和CSFramework中踢出长时间不和服务器说话的客户端。
SimpleDidadida
首先给个简单的定时器实现SimpleDidaDida类
public abstract class SimpleDidadida implements Runnable {
public static final long DEFAULT_DELAY_TIME = 1000;
private long delayTime;
private volatile boolean goon; //因为goon由startUp()方法执行的线程和run的线程控制,所以加volatile
public SimpleDidadida() {
this(DEFAULT_DELAY_TIME);
}
public SimpleDidadida(long delayTime) {
this.delayTime = delayTime;
}
public void startUp() {
if (goon == true) {
return;
}
goon = true;
new Thread(this).start();
}
public void stop() {
if (goon == false) {
return;
}
goon = false;
}
@Override
public void run() {
while (goon) {
try {
Thread.sleep(delayTime);
doSomething();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public abstract void doSomething();
}
startUp()
方法和stop()
方法是某一个线程运行的,run()
方法中也要访问goon。因此加volatile
关键字,拒绝内存优化。
这里可不可以用wait()
方法我们提出这样的疑问?所以下面我们区分下wait()
与sleep()
的区别。
- sleep()是Thread类的静态方法,wait()是Object的方法。
- sleep()不释放同步锁,wait()释放同步锁,同步锁的作用为了线程安全,限制共享资源的使用。
- sleep()可以用时间指定来使他自动醒过来,如果时间不到你只能调用interreput()来强行打断,而wait()可以用notify()直接唤起。
测试
public class Demo {
public static void main(String[] args) {
SimpleDidadida simpleDidadida = new SimpleDidadida(1000) {
@Override
public void doSomething() {
System.out.println(System.currentTimeMillis());
}
};
simpleDidadida.startUp();
try {
Thread.sleep(10000);
simpleDidadida.stop();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*运行结果
1602094091962
1602094092962
1602094093963
1602094094963
1602094095964
1602094096964
1602094097964
1602094098965
1602094099965
1602094100965
*/
可以看出还是挺精确的,是我们定的500ms,结果在人的误差接收范围(1ms)内。但是,其实考虑这样的问题,我们这个实验doing()
要做的事紧紧是个输出当前时间,很简单运行就会很快。如果我们以后的使用场景绝对不是简简单单的输出那么简单,应该有大量要做的事的代码。因此,我们继续做实验。让doing()
要做的事持续久一点。
public class Demo {
public static void main(String[] args) {
SimpleDidadida simpleDidadida = new SimpleDidadida(1000) {
@Override
public void doSomething() {
try {
Thread.sleep(500);
System.out.println(System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
simpleDidadida.startUp();
try {
Thread.sleep(10000);
simpleDidadida.stop();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*
1602094277570
1602094279070
1602094280572
1602094282073
1602094283574
1602094285075
1602094286577
*/
可以看到,如果把代码运行时间也算进去,我们简单的定时器并没有做到精准定时。
解决方法:不要在定时线程去做doing()
,因为做的事情也会消耗时间的,这个要做的事我们拿个线程去跑它!我们的定时器线程只干一件事,定时睡觉,定时起来,再启动个线程去做要做的事。
Didadida
public abstract class Didadida implements Runnable {
public static final long DEFAULT_DELAY_TIME = 1000;
private long delayTime;
private volatile boolean goon;
public Didadida() {
this(DEFAULT_DELAY_TIME);
}
public Didadida(long delayTime) {
this.delayTime = delayTime;
}
public Didadida startUp() {
if (goon == true) {
return this;
}
goon = true;
new Thread(this).start();
return this;
}
public void stop() {
if (goon == false) {
return;
}
goon = false;
}
@Override
public void run() {
while (goon) {
try {
Thread.sleep(delayTime);
new InnerWoker();
} catch (InterruptedException e) {
stop();
}
}
}
private class InnerWoker implements Runnable {
InnerWoker() {
new Thread(InnerWoker.this).start();
}
@Override
public void run() {
doing();
}
}
public abstract void doing();
}
它与简单的计时器不一样的地方是,doing()
方法是通过一个内部类启动的,也就是内部类实现这个线程,和我计时线程区别开来。每一次计时线程醒来,就去实例化一个对象,去启动要做的事。
测试
public class Demo {
public static void main(String[] args) {
Didadida didadida = new Didadida(1000) {
@Override
public void doing() {
System.out.println(System.currentTimeMillis());
}
}.startUp();
try {
Thread.sleep(10000);
didadida.stop();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*
1602095118268
1602095119268
1602095120268
1602095121269
1602095122269
1602095123269
1602095124269
1602095125270
1602095126270
1602095127271
*/
可以看到,结果是基本准确的,我们设置的计时器500ms左右。
但是,经过仔细思考,又出现新的问题。假设有如下情况,计时器1达到约定好的时间醒来了,去做要完成的事doing()
,然而这个要做的事还么完成,计时器2也醒来了也同样去做要完成的事doing()
。这时就是线程安全问题了,两个线程都在操作同样一段代码。
定时器的应用——带有动态时间的界面
import java.awt.BorderLayout;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import javax.swing.JFrame;
import javax.swing.JLabel;
import com.mec.util.Didadida;
import com.mec.util.FrameIsNull;
import com.mec.util.IMecView;
public class ActiveTimeView implements IMecView {
private JFrame jfrmMain;
private Didadida didadida;
private JLabel jlblClock;
private SimpleDateFormat sdf;
public ActiveTimeView() {
sdf = new SimpleDateFormat("HH时mm分ss秒");
initView();
}
@Override
public void init() {
jfrmMain = new JFrame("带时钟的窗口");
jfrmMain.setLayout(new BorderLayout());
jfrmMain.setSize(600, 400);
jfrmMain.setLocationRelativeTo(null);
jfrmMain.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
jlblClock = new JLabel("", JLabel.CENTER);
jlblClock.setFont(topicFont);
jlblClock.setForeground(topicColor);
jfrmMain.add(jlblClock, BorderLayout.NORTH);
}
@Override
public void reinit() {
didadida = new Didadida(333) {
@Override
public void doing() {
Calendar now = Calendar.getInstance();
jlblClock.setText(sdf.format(now.getTime()));
}
}.startUp();
}
@Override
public void dealEvent() {
jfrmMain.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
closeActiveTimeView();
}
});
}
@Override
public JFrame getJFrame() {
return jfrmMain;
}
private void closeActiveTimeView() {
didadida.stop();
try {
exitView();
} catch (FrameIsNull e) {
e.printStackTrace();
}
}
}
import com.mec.util.FrameIsNull;
public class Demo {
public static void main(String[] args) {
try {
new ActiveTimeView().showView();
} catch (FrameIsNull e) {
e.printStackTrace();
}
}
}
这个界面的时间会和我们电脑右下角时间一模一样,我保证。
并且因为我们用的定时器工具所以,时间显示是个线程跑,不会影响界面其他操作。
需要注意的是didadida = new Didadida(333)
,333ms代表333ms刷新一次JLabel,值太小刷新次数多,线程多,浪费;值太大(大于1000ms)时间就对不准了。因此控制在333ms-1000ms之内,就可以看到界面上时间准确的跳动!
总结
SimpleDidaDida简单计时器做法:Thread.sleep(delay);doing();
- 优点:不会出现线程安全问题,因为只有第一个执行完了,第二个才会执行。
- 缺点:它会导致我们的计时器计时不精准,因为
doing()
要做的事也有可能要运行一段时间,运行时间会算在我们的计时时间里。
Didadida计时器做法:Thread.sleep(delay);new InnerWoker();
- 优点:精准计时,是多长时间就是多长时间,设置的时间一到就必做要做的事。
- 缺点:可能会造成线程安全问题。解决方法有就是作为使用计时器工具的用户一定要考虑所要做的doing()能不能在规定时间做完,使延时时间大于操作时间。
其实我们做的定时器还不是最完美的,因为看到总有1~2ms的误差,误差产生原因:实例化对象是耗时的,线程的创建与销毁也是耗时的。这个误差对于一般使用者的要求可以满足,但要用到严格的工程上呢?差之毫厘,谬以千里啊!为了更精确的定时,我们可以使用线程池,这个以后再说。