【Java工具】精准定时器工具(毫秒级误差)

    科技2025-03-26  13

    需求分析

    由用户自行设定触发事件由用户提供具体工作过程在规定的触发事件到达时,自动执行具体工作过程。尽可能保证时间的精确性。

    这个工具有广泛的用途,例如轮询和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的误差,误差产生原因:实例化对象是耗时的,线程的创建与销毁也是耗时的。这个误差对于一般使用者的要求可以满足,但要用到严格的工程上呢?差之毫厘,谬以千里啊!为了更精确的定时,我们可以使用线程池,这个以后再说。

    Processed: 0.015, SQL: 8