既然要详细说说定时器,就由浅入深,先从最简单的说起。
我首先接触到的定时器就是根据线程的Thread.sleep()方法实现的,最开始学习java的时候,会用这个方法实现一些简单的动画效果,今天就来回顾一下当初的小动画!
首先 Thread.sleep(times)方法是干嘛的呢,它是用来阻塞当前线程运行的一个方法,按字面意思就是让当前线程睡一会,把CPU资源让给其他线程……你给它传入一个long参数,就是你希望她睡多久的时间值。
随便一提,我旁边的老变态面试新人的时候就喜欢问他们:sleep和wait的区别,请自行百度……
示例代码:
@Test
public void threadTimer() throws IOException {
// 每一秒钟执行一次
final long timeInterval = 1000;
Runnable runnable = new Runnable() {
public void run() {
while (true) {
// ------- code for task to run
// ------- 要运行的任务代码
System.out.println("Hello, stranger");
// ------- ends here
try {
// sleep():同步延迟数据,并且会阻塞线程
Thread.sleep(timeInterval);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//创建定时器
Thread thread = new Thread(runnable);
//开始执行
thread.start();
//阻止进程结束
System.in.read();
}
上面这个代码就是利用Thread.sleep实现一秒间隔的定时任务。之前用这种方式实现过动画效果,但是直接用它来做项目级的定时任务有点傻,这种定时器明显的缺点就是没有计算任务执行本身所花的时间。
由于执行任务的线程只有一个,所以如果某个任务的执行时间过长,那么将破坏其他任务的定时精确性。如一个任务每1秒执行一次,而另一个任务执行一次需要5秒,那么如果是固定速率的任务,那么会在5秒这个任务执行完成后连续执行5次,而固定延迟的任务将丢失4次执行。
相对于业余的Thread.sleep方法,JDK其实直接就给我们提供了简单好用的定时器–Timer
Timer是用于管理在后台执行的延迟任务或周期性任务,其中的任务使用java.util.TimerTask表示。任务的执行方式有两种:
分别演示一下:
// 两秒后执行一次任务,且只执行一次
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Timer is running");
}
}, 2000);
可以看到schedule方法有两个参数,一个是实现了TimerTask的任务参数,另一个就是执行任务的倒计时,一旦倒计时结束,就执行一次任务。
如果想间隔性的执行任务,就要用到scheduleAtFixedRate:
//表示2秒后开始执行,然后每隔5秒执行一次
Timer timer = new Timer();
timer. scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println("Timer is running");
}
}, 2000, 5000);
Timer是线程安全的,没必要担心线程问题。当然了,Timer的缺点和上面的方式一样,都是单线程执行任务,没有时间间隔的精准度。所以利用线程池来实现定时任务的ScheduledExecutorService 横空出世,因为使用了线程池,提供了良好的约定,以便精准设定执行的时间间隔。我称他为第二代定时器!!
JDK5之后,推荐使用这个类实现定时器。
先写个简单的实例:
//输入的参数2代表线程池中线程的数量。
ScheduledThreadPoolExecutor scheduled = new ScheduledThreadPoolExecutor(2);
scheduled.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.print("time:");
}
}, 0, 40, TimeUnit.MILLISECONDS);
//0表示首次执行任务的延迟时间,40表示每次执行任务的间隔时间,TimeUnit.MILLISECONDS执行的时间间隔数值单位
//意思就是:0秒后开始执行,然后每40毫秒都会执行一次任务。
其中可设置的时间单位为:
间隔单位毫秒:TimeUnit.MILLISECONDS
间隔单位秒:TimeUnit.SECONDS
间隔单位分钟:TimeUnit.MINUTES
间隔单位小时:TimeUnit.HOURS
间隔单位天:TimeUnit.DAYS
ScheduledThreadPoolExecutor 同样也提供了如同Timer的scheduled方法,用法和timer的一样,执行一次任务,不再演示。
下面我们做一个小实验,对比一下ScheduledThreadPoolExecutor和timer在执行任务的时候定时误差。
我们分别同timer和ScheduledThreadPoolExecutor写一个定时任务,都设置为2秒后执行,每秒执行一次。
如下:
static int index;
@Test
public void timer() throws IOException
{
final Timer timer = new Timer();
timer. scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
index++;
System.out.println("Timer= " + getTimes()+" " +index);
try {
//模拟任务执行需要2秒的时间
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(index >=10){
timer.cancel();
System.out.println("停止了????");
}
}
//2秒后执行,每秒执行一次
}, 2000, 1000);
System.in.read();
}
static ScheduledThreadPoolExecutor stpe = null;
@Test
public void executorTimer() throws IOException
{
// TODO code application logic here
//构造一个ScheduledThreadPoolExecutor对象,并且设置它的容量为5个
stpe = new ScheduledThreadPoolExecutor(5);
//隔2秒后开始执行任务,并且在上一次任务开始后隔1秒再执行一次;
stpe.scheduleWithFixedDelay(new Runnable()
{
@Override
public void run()
{
index++;
System.out.println("executor= " + getTimes()+" " +index);
try {
//模拟任务执行需要2秒的时间
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(index >=10){
stpe.shutdown();
if(stpe.isShutdown()){
System.out.println("停止了????");
}
}
}
}, 2, 1, TimeUnit.SECONDS);
//隔6秒后执行一次,但只会执行一次。
// stpe.schedule(task, 6, TimeUnit.SECONDS);
System.in.read();
}
private static String getTimes() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss E");
Date date = new Date();
date.setTime(System.currentTimeMillis());
return format.format(date);
}
稍微解释一下,上面有两个Test方法,和一个getTimes(返回当前时间,精确到秒)方法,其中timer是用timer实现定时器,executorTimer是用ScheduledThreadPoolExecutor实现定时器。
两个定时器都是两秒后执行,每秒执行一次,共执行十次,任务都是调用getTimers方法获取并打印当前时间,且会有一秒的线程暂停来模拟执行任务需要一秒钟。
那么我们分别运行一下看一下执行结果:
Timer:
Timer= 2020-12-25 00:09:32 Fri 1
Timer= 2020-12-25 00:09:34 Fri 2
Timer= 2020-12-25 00:09:36 Fri 3
Timer= 2020-12-25 00:09:38 Fri 4
Timer= 2020-12-25 00:09:40 Fri 5
Timer= 2020-12-25 00:09:42 Fri 6
Timer= 2020-12-25 00:09:44 Fri 7
Timer= 2020-12-25 00:09:46 Fri 8
Timer= 2020-12-25 00:09:48 Fri 9
Timer= 2020-12-25 00:09:50 Fri 10
停止了????
ScheduledThreadPoolExecutor:
executor= 2020-12-25 00:16:38 Fri 1
executor= 2020-12-25 00:16:41 Fri 2
executor= 2020-12-25 00:16:44 Fri 3
executor= 2020-12-25 00:16:47 Fri 4
executor= 2020-12-25 00:16:50 Fri 5
executor= 2020-12-25 00:16:53 Fri 6
executor= 2020-12-25 00:16:56 Fri 7
executor= 2020-12-25 00:16:59 Fri 8
executor= 2020-12-25 00:17:02 Fri 9
executor= 2020-12-25 00:17:05 Fri 10
停止了????
可以看出来Timer是用了两秒执行一次,而使用了传说中更稳定的ScheduledThreadPoolExecutor却是3秒执行一次,而两者都是设定的每秒执行一次,所以我的测试结果是ScheduledThreadPoolExecutor误差更大……
我会继续查找资料,找一下这个问题出现的原因,然后更新在此。
-----------------------------------------------------------分割线--------------------------------------------------------------------------
根据进一步的查阅和调试,发现之前做的实验本身有问题:
ScheduledThreadPoolExecutor执行定时任务有三种方法:
我上面实验用的是scheduleWithFixedDelay 方法,该方法是任务执行完之后,再计时间隔时间,然后执行下一个任务。上面设置的任务间隔为1秒,每个任务大约花费2秒,所以两次打印间隔三秒。这就能说的通了,所以使用了线程池,提供了良好的约定,以便精准设定执行的时间间隔的那个定时人无方法,其实应该是ScheduledThreadPoolExecutor的scheduleAtFixedRate 吗?
其实并不是我们想的那样scheduleAtFixedRate能够多线程执行任务,ScheduledThreadPoolExecutor比我想的更有趣,下一篇继续为大家揭秘:详解:被人误解的ScheduledThreadPoolExecutor定时器