环境:Amazon Linux AMI 上的 Java 7(已修补)。请注意,在此环境中使用 NTP。
我们的部分代码会定期执行计划任务。通常,执行的确切时间并不是特别重要。但是,我们有类似的逻辑用于计算报告间隔。报告期预计将落在确切的小时边界上。
示例代码:
protected Calendar increment(ScheduledPeriodEnum scheduledPeriod, Calendar date) {
Calendar next = CalendarUtils.getCalendar();
next.setTimeInMillis(date.getTimeInMillis());
switch (scheduledPeriod) {
case ONE_DAY:
next.add(Calendar.DAY_OF_MONTH, 1);
break;
case ONE_HOUR:
next.add(Calendar.HOUR_OF_DAY, 1);
break;
case FIFTEEN_MINUTE:
next.add(Calendar.MINUTE, 15);
break;
case ONE_MONTH:
next.add(Calendar.MONTH, 1);
break;
default:
throw new RuntimeException("Unhandled case: " + scheduledPeriod);
}
return next;
}
我们将时间持久化为 unix 时间戳(长)值。Calendar
实例均采用 UTC 时区。
我们的理解是,Java 7的日历实现不考虑闰秒。我们还相信NTP将更新操作系统时钟。
我们知道亚马逊2012年的闰秒崩溃。我们正在直接与他们沟通,以了解他们的运营准备情况。
具体问题:
如果我们
increment
的日历在闰秒之前的一小时边界上,它会在闰秒之后的小时边界上吗?还是整点前一秒?我们应该如何测试/验证这段代码将如何跨闰秒边界运行?被黑客入侵的NIST跳跃文件是一个好方法吗?
是否有更合适的模式/实现可以回避闰秒问题?
我不会太担心 2015 年的下一个闰秒,并假设 Linux 团队在此期间做了很多工作来解决闰秒处理代码的任何问题,另请参阅对 Linus Torvalds 的有趣采访。如果Linux软件再次出错,那么你只能重新启动Java程序(这里Java只是后端)
现在让我们考虑一下可能会发生什么。关于完全不知道任何闰秒的代码,如java.util.Date
和java.util.Calendar
的东西,请记住,这样的代码只看到操作系统时钟提供的内容。大多数操作系统只提供 UNIX 时间戳。如果它们与 NTP 同步,则还请记住,NTP 时间戳不计算 NTP 协议中指定的闰秒。它们只是重复相同的时间戳。然后,Windows可能会在以后随时触发时钟跳跃,而Linux内核尝试更精确,应用一些闰秒处理代码并立即操作系统时钟。无论如何,两个操作系统都只提供类似POSIX的时间戳。而Java只是看到...无。
这也回答了您的第一个特定问题:GregorianCalendar
对象在添加一小时后将保留在小时边界上。
关于你的第二个问题:
这里只有一秒钟的问题。但可能更麻烦的是,本地时钟甚至可能出错几分钟(然后在与 NTP 时钟同步后突然跳跃)。后一种行为可能随时发生,而不仅仅是在2015-06-30年底。所以我认为你真正的问题是时钟的单调性。这通常表明,任何测试都应该使用可注射时钟机制之类的东西。例如,您可以编写一个 TimeSource
接口,该接口通过方法public long currentTime()
生成任何 unix 时间戳 (甚至可以进行由计时器创建的模拟跳转),然后在 JUnit-test 类中使用此接口来提供(假)时间并观察代码的行为。
公元 3.)大多数人只会使用一个不知道任何闰秒的库,因为 POSIX 很容易理解和计算(尽管当涉及到这些特殊的秒数时,它不正确)。如果没有闰秒处理代码,就很难有直接植根于此类标准库的问题。否则,如果您愿意不向用户隐藏闰秒,那么您也可以在此帖子中查阅我的答案。
无论你的决定是什么,闰秒真的不够重要,无法选择合适的库/实现(线程安全、国际化等其他主题要重要得多)。关于这些标准,旧的Calendar
东西非常糟糕。