这个工具有广泛的用途,例如轮询和CSFramework中踢出长时间不和服务器说话的客户端。
首先给个简单的定时器实现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(),因为做的事情也会消耗时间的,这个要做的事我们拿个线程去跑它!我们的定时器线程只干一件事,定时睡觉,定时起来,再启动个线程去做要做的事。
它与简单的计时器不一样的地方是,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()。这时就是线程安全问题了,两个线程都在操作同样一段代码。
这个界面的时间会和我们电脑右下角时间一模一样,我保证。
并且因为我们用的定时器工具所以,时间显示是个线程跑,不会影响界面其他操作。
需要注意的是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的误差,误差产生原因:实例化对象是耗时的,线程的创建与销毁也是耗时的。这个误差对于一般使用者的要求可以满足,但要用到严格的工程上呢?差之毫厘,谬以千里啊!为了更精确的定时,我们可以使用线程池,这个以后再说。