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

需求分析

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

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

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用中提到,我们可以通过创建Timer类的对象来使用定时器,并通过调用schedule方法来设置定时任务。这个方法接受一个TimerTask对象作为参数,TimerTask对象包含了要执行的任务以及执行时间。在这个例子中,任务是通过重写run方法来实现的,当计时器到达指定时间时,run方法会被执行。 引用中提到了一个自定义的MyTimer类,它使用了一个线程来管理定时任务。在这个类中,任务被放入一个队列中,并且通过比较任务的执行时间和当前时间来决定是否执行任务。如果当前时间大于等于任务的执行时间,就执行任务;如果当前时间小于任务的执行时间,就将任务再次放回队列中,并通过等待来延迟执行。 引用提供了一个用来描述定时任务的类,即MyTask类。这个类包含了要执行的任务以及执行时间的信息。任务是通过runnable接口来实现的,时间使用时间戳表示。 根据这些引用内容,可以得出结论,Java定时器是可以实现毫秒的精确定时的。通过使用Timer类或自定义的MyTimer类,结合TimerTask或MyTask类,可以设置毫秒的定时任务。具体实现方式包括使用schedule或schedule方法来设置任务,并通过比较当前时间和任务执行时间来决定是否执行任务。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Java定时器](https://blog.csdn.net/m0_67683346/article/details/126978831)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值