前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java定时器(Timer)「建议收藏」

Java定时器(Timer)「建议收藏」

作者头像
全栈程序员站长
发布2022-09-08 11:58:46
1.4K0
发布2022-09-08 11:58:46
举报
文章被收录于专栏:全栈程序员必看

大家好,又见面了,我是你们的朋友全栈君。

1.介绍

Timer和TimerTask是用于在后台线程中调度任务的java util类。简单地说,TimerTask是要执行的任务,Timer是调度器。

2.调度一次性任务

2.1 指定延迟后执行

让我们从简单地在定时器的帮助下运行单个任务开始:

代码语言:javascript
复制
@Test
public void givenUsingTimer_whenSchedulingTaskOnce_thenCorrect() { 
   
    TimerTask task = new TimerTask() { 
   
        public void run() { 
   
            System.out.println("Task performed on: " + new Date() + "n" +
              "Thread's name: " + Thread.currentThread().getName());
        }
    };
    Timer timer = new Timer("Timer");
    
    long delay = 1000L;
    timer.schedule(task, delay);
}

延迟时间作为schedule()方法的第二个参数给出。我们将在下一节中了解如何在给定的日期和时间执行任务。

注意,如果我们正在运行这是一个JUnit测试,我们应该添加一个Thread.sleep(delay*2)调用,以允许定时器的线程在JUnit测试停止执行之前运行任务。

2.2 指定时间执行

现在,让我们看看Timer#schedule(TimerTask,Date)方法,它将日期而不是long作为其第二个参数,这实现了在某个时刻而不是在延迟之后执行任务。

这一次,让我们假设我们有一个旧的遗留数据库,我们希望将它的数据迁移到一个具有更好模式的新数据库中。我们可以创建一个DatabaseMigrationTask类来处理该迁移:

代码语言:javascript
复制
public class DatabaseMigrationTask extends TimerTask { 
   
    private List<String> oldDatabase;
    private List<String> newDatabase;

    public DatabaseMigrationTask(List<String> oldDatabase, List<String> newDatabase) { 
   
        this.oldDatabase = oldDatabase;
        this.newDatabase = newDatabase;
    }

    @Override
    public void run() { 
   
        newDatabase.addAll(oldDatabase);
    }
}

为简单起见,我们用字符串列表来表示这两个数据库。简单地说,我们的迁移就是将第一个列表中的数据放到第二个列表中。要在所需的时刻执行此迁移,我们必须使用schedule()方法的重载版本:

代码语言:javascript
复制
List<String> oldDatabase = Arrays.asList("Harrison Ford", "Carrie Fisher", "Mark Hamill");
List<String> newDatabase = new ArrayList<>();

LocalDateTime twoSecondsLater = LocalDateTime.now().plusSeconds(2);
Date twoSecondsLaterAsDate = Date.from(twoSecondsLater.atZone(ZoneId.systemDefault()).toInstant());

new Timer().schedule(new DatabaseMigrationTask(oldDatabase, newDatabase), twoSecondsLaterAsDate);

我们将迁移任务和执行日期赋予schedule()方法。然后,在twoSecondsLater指示的时间执行迁移:

代码语言:javascript
复制
while (LocalDateTime.now().isBefore(twoSecondsLater)) { 
   
    assertThat(newDatabase).isEmpty();
    Thread.sleep(500);
}
assertThat(newDatabase).containsExactlyElementsOf(oldDatabase);

虽然我们在这一刻之前,迁移并没有发生。

3.调度一个可重复执行任务

既然我们已经讨论了如何安排任务的单个执行,那么让我们看看如何处理可重复的任务。同样,Timer类提供了多种可能性:我们可以将重复设置为观察固定延迟或固定频率。

  • 固定延迟:意味着执行将在最后一次执行开始后的一段时间内开始,即使它被延迟(因此它本身被延迟)。假设我们想每两秒钟安排一个任务,第一次执行需要一秒钟,第二次执行需要两秒钟,但是延迟了一秒钟。然后,第三次执行将从第五秒开始:
  • 固定频率:意味着每次执行都将遵守初始计划,无论之前的执行是否被延迟。让我们重用前面的示例,使用固定的频率,第二个任务将在3秒钟后开始(因为延迟)。但是,四秒钟后的第三次执行(关于每两秒钟执行一次的初始计划):

关于这两种调度方式,让我们看看如何使用它们:

为了使用固定延迟调度,schedule()方法还有两个重载,每个重载都使用一个额外的参数来表示以毫秒为单位的周期性。为什么两次重载?因为仍然有可能在某个时刻或某个延迟之后开始执行任务。

至于固定频率调度,我们有两个scheduleAtFixedRate()方法,它们的周期也是以毫秒为单位的。同样,我们有一种方法可以在给定的日期和时间启动任务,还有一种方法可以在给定的延迟后启动任务。

注意一点:如果一个任务的执行时间超过了执行周期,那么无论我们使用固定延迟还是固定速率,它都会延迟整个执行链。

3.1 固定延迟

现在,让我们设想一下,我们要实现一个通讯系统,每周向我们的追随者发送一封电子邮件。在这种情况下,重复性任务似乎是理想的。所以,让我们安排每秒钟的通讯,这基本上是垃圾邮件,但由于发送是假的,所以不用在意:)

让我们首先设计一个任务:

代码语言:javascript
复制
public class NewsletterTask extends TimerTask { 
   
    @Override
    public void run() { 
   
        System.out.println("Email sent at: " 
          + LocalDateTime.ofInstant(Instant.ofEpochMilli(scheduledExecutionTime()), 
          ZoneId.systemDefault()));
    }
}

每次执行时,任务都会打印其调度时间,我们使用TimerTask#scheduledExecutionTime()方法收集这些时间。那么,如果我们想在固定延迟模式下每秒钟安排一次这个任务呢?我们必须使用前面提到的schedule()的重载版本:

代码语言:javascript
复制
new Timer().schedule(new NewsletterTask(), 0, 1000);

for (int i = 0; i < 3; i++) { 
   
    Thread.sleep(1000);
}

当然,我们只对少数情况进行测试:

Email sent at: 2020-01-01T10:50:30.860 Email sent at: 2020-01-01T10:50:31.860 Email sent at: 2020-01-01T10:50:32.861 Email sent at: 2020-01-01T10:50:33.861

如上所示,每次执行之间至少有一秒钟的间隔,但有时会延迟一毫秒。这种现象是由于我们决定使用固定延迟重复。

3.3 调度一个每日任务

代码语言:javascript
复制
@Test
public void givenUsingTimer_whenSchedulingDailyTask_thenCorrect() { 
   
    TimerTask repeatedTask = new TimerTask() { 
   
        public void run() { 
   
            System.out.println("Task performed on " + new Date());
        }
    };
    Timer timer = new Timer("Timer");
    
    long delay = 1000L;
    long period = 1000L * 60L * 60L * 24L;
    timer.scheduleAtFixedRate(repeatedTask, delay, period);
}

4.取消调度器和任务

4.1 在Run方法中去掉调度任务

在run()方法对TimerTask本身的实现中调用TimerTask.cancel()方法:

代码语言:javascript
复制
@Test
public void givenUsingTimer_whenCancelingTimerTask_thenCorrect()
  throws InterruptedException { 
   
    TimerTask task = new TimerTask() { 
   
        public void run() { 
   
            System.out.println("Task performed on " + new Date());
            cancel();
        }
    };
    Timer timer = new Timer("Timer");
    
    timer.scheduleAtFixedRate(task, 1000L, 1000L);
    
    Thread.sleep(1000L * 2);
}

4.2 取消定时器

调用Timer.cancel()方法:

代码语言:javascript
复制
@Test
public void givenUsingTimer_whenCancelingTimer_thenCorrect() 
  throws InterruptedException { 
   
    TimerTask task = new TimerTask() { 
   
        public void run() { 
   
            System.out.println("Task performed on " + new Date());
        }
    };
    Timer timer = new Timer("Timer");
    
    timer.scheduleAtFixedRate(task, 1000L, 1000L);
    
    Thread.sleep(1000L * 2); 
    timer.cancel(); 
}

5.Timer对比ExecutorService

我们也可以使用ExecutorService来安排定时器任务,而不是使用定时器。下面是一个在指定间隔运行重复任务的快速示例:

代码语言:javascript
复制
@Test
public void givenUsingExecutorService_whenSchedulingRepeatedTask_thenCorrect() 
  throws InterruptedException { 
   
    TimerTask repeatedTask = new TimerTask() { 
   
        public void run() { 
   
            System.out.println("Task performed on " + new Date());
        }
    };
    ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    long delay  = 1000L;
    long period = 1000L;
    executor.scheduleAtFixedRate(repeatedTask, delay, period, TimeUnit.MILLISECONDS);
    Thread.sleep(delay + period * 3);
    executor.shutdown();
}

那么定时器和ExecutorService解决方案之间的主要区别是什么:

  • 定时器对系统时钟的变化敏感;ScheduledThreadPoolExecutor并不会。
  • 定时器只有一个执行线程;ScheduledThreadPoolExecutor可以配置任意数量的线程。
  • TimerTask中抛出的运行时异常会杀死线程,因此后续的计划任务不会继续运行;使用ScheduledThreadExecutor–当前任务将被取消,但其余任务将继续运行。

6.本文代码

Java定时器样例代码

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/156388.html原文链接:https://javaforall.cn

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.介绍
  • 2.调度一次性任务
    • 2.1 指定延迟后执行
      • 2.2 指定时间执行
      • 3.调度一个可重复执行任务
        • 3.1 固定延迟
          • 3.3 调度一个每日任务
          • 4.取消调度器和任务
            • 4.1 在Run方法中去掉调度任务
              • 4.2 取消定时器
              • 5.Timer对比ExecutorService
              • 6.本文代码
              相关产品与服务
              数据库
              云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档