如何实现一个精准的线程定时器?Java定时任务实现教程

 更新时间:2026年07月14日 09:30:21   作者:Arrvine  
还在为Java定时器不准烦恼吗?本文带你从简陋版线程定时器入手,分析为什么wait后执行doing会导致误差,并给出进阶方案:用内部类分离任务线程,让定时更稳定,无论你是初学者还是想优化定时任务,都能从中获得实用技巧,轻松实现高精度定时

需求

在某些应用场合,需要定时地完成一些操作,希望能有一个“精准”的定时的工具,方便编程。

应该有一个线程,这个线程每经过一段时间(这个时间可以指定),就“醒来”,并执行外部要求完成的操作。

注:该定时器的编写是基于线程的,如对线程知识毫无了解,请绕道而行。

分析

1.作为一个定时器,需要定义一个时延,即要求等待的时间delay;

2.需要用线程来实现该定时器,便需要一个变量来控制线程goon;

3.既然存在线程,必然要给两个方法,开启线程start(),关闭线程stop();

4.定时器要应用在某些场合来做一些其他事情,那么就需要一个方法来负责做某些事,但是由于现在无法获悉未来要做什么,所以给一个抽象方法doing(),未来由其他类来实现该方法。

具体实现

最简陋版的定时器

public abstract class SimpleDidaDida implements Runnable {
	public static final long DEFAULT_DELAY=1000;//默认时延
	private long delay;//时延
	private volatile boolean goon;//便于控制线程
	private Object lock;//确保可以精准等待,给一个对象锁,wait()等基于该锁
	
	//初始化时允许用户指定等待时间,若不指定,则使用默认时延
	public SimpleDidaDida() {
		this(DEFAULT_DELAY);//调用单参构造
	}
	
	public SimpleDidaDida(long delay) {
		this.lock = new Object();//初始化对象锁
		this.delay = delay;
	}
	
	//具体完成的事情不由该类实现
	public abstract void doing();

	//启动线程
	public void start() {
		if(this.goon == true) {
			return;
		}
		this.goon = true;
		new Thread(this).start();
	}
	
	//停止线程,当线程等待执行完doing后,goon==false时
	public void stop() {
		if(this.goon == false) {
			return;
		}
		this.goon = false;//说明线程运行完毕
	}
	
	@Override
	public void run() {
		while(goon) {
			synchronized(lock) {
				try {
					//当前线程暂停执行,即处于等待状态,并释放对象锁;
					//当超过delay或执行notify/notifyAll()方法,该线程会被唤醒
					lock.wait(delay);	
					doing();//调用抽象方法
				} catch (InterruptedException e) {
					goon = false;
				}
}

实现doing()方法的类:

public class DemoSimpleDidaDida {
	private SimpleDidaDida dida;
	private static int times = 0;
	
	public DemoSimpleDidaDida() {
		//此处500,表示500毫秒运行一次
		dida = new SimpleDidaDida(500) {
			@Override
			public void doing() {
				System.out.println(System.currentTimeMillis());
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		};
	}
	
	public void runup() {
		dida.start();//启动线程
		try {
			Thread.sleep(5000);//此处要求线程运行5秒后停止,5000单位是毫秒(ms)
		} catch (InterruptedException e) {
		}
		dida.stop();//停止线程
	}
	
}

实例化DemoSimpleDidaDida类:

public class Test {

	public static void main(String[] args) {
		new DemoSimpleDidaDida().runup();
	}
}

运行结果:

上述图片中的数字单位都是毫秒,只需要注意后四位即可,第二个数字与第一个数字相差604,第三个数字与第二个数字相差602,第四个数字与第三个数字相差601,之后的数字相差也并不是500ms.

为什么会这样呢?不妨考虑一下,在运行wait(delay)方法之后,运行了doing()方法,我们在doing()方法中添加了Thread.sleep(100),而运行结果显示相差时间都在600ms左右,这足以说明结果不准确都是因为doing()方法本身就是需要花费时间的。这就导致了定时器并不准确。

进阶版

为了增加定时器的准确性,不能再wait()方法执行后去执行doing()方法。

为了保证该线程的纯粹性,我们选择使用线程,wait()之后再启动一个线程,来专门执行doing(),而这个线程的实现我选择内部类来实现。

为了避免冗余,下面只列出被改动部分的代码

@Override
	public void run() {
		while(goon) {
			synchronized(lock) {
				try {
					//当前线程暂停执行,即处于等待状态,并释放对象锁;
					//当超过delay或执行notify/notifyAll()方法,该线程会被唤醒
					lock.wait(delay);	
					new InnerWorker();//内部类启动线程
				} catch (InterruptedException e) {
					goon = false;
				}
			}
		}
	}
	
	//该内部类主要负责启动一个线程,让该线程去执行doing().
	private class InnerWorker implements Runnable{

		public InnerWorker() {
			new Thread(this).start();
		}
		
		@Override
		public void run() {
			doing();
		}		
	}

在上述程序中,wait()执行完后执行了内部类,虽然还是有时间的损耗,但是因为作用是启动线程,所以每次损耗的时间都是一个固定的值,方便控制。

运行结果:

是不是相对工整了很多,只有第四次和第五次相差501,其他都只相差500ms.

但是该模式下有一个问题:如果线程第二次开始执行,第一次doing()还未执行完,该如何?

答:这种情况其实是建立在doing() 的运行时间超过线程的休眠时间之上的。但其实并没有什么影响,因为每一个线程都是独立运行的,我们的需求是做一个定时器,要求每次线程定时启动即可,不要求doing()按顺序执行。

为了验证该问题,我对代码进行微小的修改,来证明这个问题.输出时间时,加上这是第几次运行的。

public class DemoSimpleDidaDida {
	private SimpleDidaDida dida;
	private static int times = 0;
	
	public DemoSimpleDidaDida() {
		dida = new SimpleDidaDida(500) {
			@Override
			public void doing() {
				nt time = ++times;
				System.out.println("第" + time+ "次开始:" + System.currentTimeMillis());
				try {
					Thread.sleep(600);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("第" + time+ "次结束:" + System.currentTimeMillis());

			}	
			}		
		};
	}
	
	public void runup() {
		dida.start();
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
		}
		dida.stop();
	}
	
}

如图所示,其实每次执行的线程之间都是独立运行,不会影响其他线程的运行结果。

目前为止,定时器还不是很完善,定时结果也不是非常精准,后续会有更加精准的方案,届时会使用线程池来完成,定时将更加精准。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 浅析Alibaba Nacos注册中心源码剖析

    浅析Alibaba Nacos注册中心源码剖析

    这篇文章主要介绍了浅析Alibaba Nacos注册中心源码剖析,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-05-05
  • Java服务调用失败报Service com.oneinfinite.adflow.api.service.TestService未找到的解决方法

    Java服务调用失败报Service com.oneinfinite.adflow.api.service.T

    在Java开发中,服务调用是常见的操作,尤其是在微服务架构中,然而,服务调用过程中可能会遇到各种问题,下面我们来看看如何解决Service com.oneinfinite.adflow.api.service.TestService with version 0.0.0 not found的问题吧
    2025-03-03
  • 聊聊Spring Cloud Cli 初体验

    聊聊Spring Cloud Cli 初体验

    这篇文章主要介绍了聊聊Spring Cloud Cli 初体验,SpringBoot CLI 是spring Boot项目的脚手架工具。非常具有实用价值,需要的朋友可以参考下
    2018-04-04
  • Spring EnableAsync注解异步执行源码解析

    Spring EnableAsync注解异步执行源码解析

    这篇文章主要为大家介绍了Spring EnableAsync注解源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • Spring+MyBatis实现数据库读写分离方案

    Spring+MyBatis实现数据库读写分离方案

    本文主要介绍了Spring+MyBatis实现数据库读写分离方案。具有一定的参考价值,下面跟着小编一起来看下吧
    2017-01-01
  • Java日常练习题,每天进步一点点(36)

    Java日常练习题,每天进步一点点(36)

    下面小编就为大家带来一篇Java基础的几道练习题(分享)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧,希望可以帮到你
    2021-07-07
  • 使用java文件过滤器输出制定格式文件路径的实例代码

    使用java文件过滤器输出制定格式文件路径的实例代码

    这篇文章主要介绍了使用java文件过滤器输出制定格式文件路径的方法,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-11-11
  • Spring AI内置DeepSeek的详细步骤

    Spring AI内置DeepSeek的详细步骤

    Spring AI 最新快照版已经内置 DeepSeek 了,所以以后项目中对接 DeepSeek 就方便多了,但因为快照版会有很多 Bug,所以今天咱们就来看稳定版的 Spring AI 如何对接 DeepSeek 满血版,感兴趣的小伙伴跟着小编一起来看看吧
    2025-02-02
  • java 集合----Map、Collection

    java 集合----Map、Collection

    HashSet 的 add() 方法添加集合元素时实际上转变为调用 HashMap 的 put()方法来添加 key-value 对。HashMap 的 put() 方法.hashCode() 判断返回值相等,如果返回值相等则再通过 equals 比较也返回 true,最终认为key对象是相等的已经在HashMap中存在。
    2016-04-04
  • Java基础之垃圾回收机制详解

    Java基础之垃圾回收机制详解

    这篇文章主要介绍了Java基础之垃圾回收机制详解,文中有非常详细的代码示例,对正在学习java基础的小伙伴们有非常好的帮助,需要的朋友可以参考下
    2021-04-04

最新评论