一文带你搞懂Java定时器Timer的使用
作者:熬夜磕代码丶 发布时间:2022-09-08 01:18:16
一、定时器是什么
定时器类似于我们生活中的闹钟,可以设定一个时间来提醒我们。
而定时器是指定一个时间去执行一个任务,让程序去代替人工准时操作。
标准库中的定时器: Timer
方法 | 作用 |
---|---|
void schedule(TimerTask task, long delay) | 指定delay时间之后(单位毫秒)执行任务task |
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时器任务! ");
}
},1000);
}
这段程序就是创建一个定时器,然后提交一个1000s后执行的任务。
二、自定义定时器
我们自己实现一个定时器的前提是我们需要弄清楚定时器都有什么:
1.一个扫描线程,负责来判断任务是否到时间需要执行
2.需要有一个数据结构来保存我们定时器中提交的任务
创建一个扫描线程相对比较简单,我们需要确定一个数据结构来保存我们提交的任务,我们提交过来的任务,是由任务和时间组成的,我们需要构建一个Task对象,数据结构我们这里使用优先级队列,因为我们的任务是有时间顺序的,具有一个优先级,并且要保证在多线程下是安全的,所以我们这里使用:PriorityBlockingQueue比较合适。
首先我们构造一个Task对象
class MyTask {
//即将执行的任务
private Runnable runnable;
//在多久后执行
private long time;
public MyTask(Runnable runnable, long time) {
this.runnable = runnable;
this.time = time;
}
public long getTime() {
return time;
}
//执行任务
public void run() {
runnable.run();
}
}
MyTimer类:
public class MyTimer {
//扫描线程
private Thread t;
//创建一个阻塞优先级队列,用来保存提交的Task对象
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
private Object locker = new Object();
//提交任务的方法
public void schedule(Runnable runnable,long time) {
//这里我们的时间换算一下,保存实际执行的时间
MyTask task = new MyTask(runnable,System.currentTimeMillis() + time);
queue.put(task);
}
//构建扫描线程
public MyTimer() {
t = new Thread(() -> {
//我们取出队列中时间最近的元素
while (true) {
try {
MyTask task = queue.take();
long curTime = System.currentTimeMillis();
if(curTime < task.getTime()) {
//证明还没到执行的时间,再放进队列
queue.put(task);
} else {
//到时间了,执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
t.start();
}
虽然我们大体已经写出来了,但是我们这个定时器实现的还有一些问题。
问题1:既然我们是优先级队列,我们再阻塞优先级队列中放入Task对象时,是根据什么建立堆的?
我们发现当我们运行程序时,我们的程序也会报这样的错误。
class MyTask implements Comparable<MyTask>{
//即将执行的任务
private Runnable runnable;
//在多久后执行
private long time;
public MyTask(Runnable runnable, long time) {
this.runnable = runnable;
this.time = time;
}
public long getTime() {
return time;
}
//执行任务
public void run() {
runnable.run();
}
@Override
public int compareTo(MyTask o) {
return (int) (this.time - o.time);
}
}
我们需要实现Comparable接口并且重写compareTo方法,指明我们是根据时间来决定在队列中的优先级。
2.我们的扫描线程,扫描的速度太快,造成了不必要的CPU资源浪费。
比如我们早上8.00提交了一个中午12.00的任务,那么我们这样的程序就会从8.00一直循环几十亿次,而这样的等待是没有任何意义的。
更合理的方式是,不要在这里忙等,而是“阻塞式”等待。
public MyTimer() {
t = new Thread(() -> {
//我们取出队列中时间最近的元素
while (true) {
try {
MyTask task = queue.take();
long curTime = System.currentTimeMillis();
if(curTime < task.getTime()) {
//证明还没到执行的时间,再放进队列
queue.put(task);
synchronized (locker) {
locker.wait(task.getTime() - curTime);
}
} else {
//到时间了,执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
t.start();
我们重写一下扫描线程,进行修改,当我们判断队列中最近的一个任务的时间都没到时,我们的扫描线程就进行阻塞等待,这里我们使用的不是wait(),而是wait(long time),我们传入的参数是要执行的时间和当前时间的差值,有的同学可能会问了,那这样执行的时候和预期执行的时间不就有出入了嘛?
因为我们程序里的定时操作,本来就难以做到非常准确,因为操作系统调度是随机的,有一定的时间开销,存在ms的误差都是相当正常的,不影响我们的正常使用。
我们上面进行阻塞等待,难道就傻傻的等到时间到了自动唤醒嘛? 有没有啥特殊情况呢?这里是有的,比如我们设定了一个阻塞到12点在唤醒,但我们又提交了一个10点的新任务,那么我们就应该提前唤醒了,所以我们应该在每次提交任务后都进行主动唤醒,再由我们扫描线程决定是执行还是继续阻塞等待。
public void schedule(Runnable runnable,long time) {
//这里我们的时间换算一下,保存实际执行的时间
MyTask task = new MyTask(runnable,System.currentTimeMillis() + time);
queue.put(task);
synchronized (locker) {
locker.notify();
}
}
即使我们现在所有正常的情况都考虑到了,但是我们这里仍然存在一种极端的情况。
假设我们的扫描线程刚执行完put方法,这个线程就被cpu调度走了,此时我们的另一个线程调用了schedule,添加了新任务,新任务是10点执行,然后notify,因为我们并没有wait(),所以相当于这里是空的notify,然后我们的线程调度回来去执行wait()方法,但是我们的时间差仍然是之前算好的时间差,从8.00点到12.00点,这样就会产生很大的错误。
这里造成这样的问题,是因为我们的take操作和wait操作不是原子的,我们需要在take和wait之间加上锁,保证每次notify的时候,都在wait中。
public MyTimer() {
t = new Thread(() -> {
while (true) {
try {
synchronized (locker) {
MyTask Task = queue.take();
long curTime = System.currentTimeMillis();
if (curTime < Task.getTime()) {
queue.put(Task);
locker.wait(Task.getTime() - curTime);
} else {
Task.run();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
来源:https://blog.csdn.net/buhuisuanfa/article/details/128582096
猜你喜欢
- java 反射机制:测试实体类以Human为例/** * Project: Day12_for_lxy * Created: Lulu *
- 1集合的概念把集合看做是一个容器,集合不是一个类,是一套集合框架,框架体系包含很多的集合类,java api提供了集合存储任意类型(基本包装
- 安装jdk(介绍三种方法)查看java版本:java -version方法一:利用yum源来安装jdk(此方法不需要配置环境变量)查看yum
- spring FactoryBean 是创建 复杂的bean,一般的bean 直接用xml配置即可,如果一个bean的创建过程中
- 1.背景Java语言相比于C和C++,一个最大的特点就是不需要程序员自己手动去申请和释放内存,这一切交由JVM来完成。在Java中,运行时的
- 一. 概述参考开源项目https://github.com/xkcoding/spring-boot-demo在系统运维中, 有时候为了避免
- 前言最近学习netty的时候发现nio包下有个FileChannel类,经过了解这个类作用是个专门负责传输文件的通道,支持多线程,而且经过反
- 前言总是觉得对HashMap很熟悉,但最近连续被问到几个关于它的问题,才发现它其实并不简单。这里对关于它的一些问题做个总结,也希望能够大家一
- 相信大家肯定都在电商网站买过东西,当我们看中一件喜欢又想买的东西时,这时候你又不想这么快结账,这时候你就可以放入购物车;就像我们平时去超市买
- 集成使用1、添加 gradle 依赖implementation "com.ctrip.framework.apollo:apol
- 谈到 Java 的线程池最熟悉的莫过于 ExecutorService 接口了,jdk1.5 新增的 java.util.concurren
- spring boot诞生的背景在spring boot出现以前,使用spring框架的程序员是这样配置web应用环境的,需要大量的xml配
- 今天想说的就是能够在我们操作数据库的时候更简单的更高效的实现,现成的CRUD接口直接调用,方便快捷,不用再写复杂的sql,带吗简单易懂,话不
- Springboot 实体类生成数据库表JPA:springboot -jpa:数据库的一系列的定义数据持久化的标准的体系学习的目的是:利用
- 最近在做项目开始,涉及到服务器与安卓之间的接口开发,在此开发过程中发现了安卓与一般浏览器不同,安卓在每次发送请求的时候并不会带上上一次请求的
- Java Set集合的遍历及实现类的比较Java中Set集合是一个不包含重复元素的Collection,首先我们先看看遍历方法package
- 说到事件机制,可能脑海中最先浮现的就是日常使用的各种 listener,listener去监听事件源,如果被监听的事件有变化就会通知list
- 1.如图所示,Spring配置文件应该带有是树叶标识,但此处显示的为普通的properties文件2.选择Open Module Setti
- 其实这个表示有点不太对,应该是 Druid 动态切换数据源的方法,只是应用在了 springboot 框架中,准备代码准备了半天,之前在一次
- 一棵二叉查找树是按二叉树结构来组织的。这样的树可以用链表结构表示,其中每一个结点都是一个对象。结点中除了数据外,还包括域left,right