在Java中如何测量时间经过?

361

我想要类似于这样的东西:

public class Stream
{
    public startTime;
    public endTime;

    public getDuration()
    {
        return startTime - endTime;
    }
}

在Java中,要实现这个目标,重要的是要考虑好以下情况:如果开始时间是23:00,结束时间是1:00,则需要获得2:00的持续时间。


1
可能存在重复问题:https://dev59.com/InVC5IYBdhLWcg3wz0h9,https://dev59.com/tXRB5IYBdhLWcg3wn4fc和https://dev59.com/pnRB5IYBdhLWcg3wz6UK。 - Peter Mortensen
1
链接问题 不是重复的。那个问题的标题是误导性的。那个问题的内容是关于从计算机时钟访问当前时间。答案涉及 System.currentTimeMillis()System.nanoTime()。这里的问题侧重于计算两个时刻之间经过的时间,而不考虑获取当前时间。 - Basil Bourque
15个回答

710

很不幸,到目前为止发布的10个答案都不太正确。

如果你要测量经过的时间,并且想要结果准确,你必须使用System.nanoTime()。你不能使用System.currentTimeMillis(),除非你不在意结果是否正确。

nanoTime 的目的是测量 经过的 时间,而 currentTimeMillis 的目的是测量 墙上挂钟 时间。你不能把一个用于另一个目的。原因是没有任何一台计算机的时钟是完美的;它总是会漂移并且偶尔需要校正。这种校正可能是手动进行的,或者在大多数机器上,有一个运行的进程会持续发出小的修正以调整系统时钟(“墙上挂钟”)。这些修正经常发生。还有一种这样的校正是在出现闰秒的时候发生。

由于nanoTime的目的是测量经过的时间,所以它不受这些微小校正的影响。它是你想要使用的。任何正在使用currentTimeMillis进行的计时都会出错--甚至可能为负数。

你可能会说,“这听起来好像永远不会真正有那么大的影响”,对此我要说,也许不会,但总体上,正确的代码难道不比错误的代码更好吗?此外,nanoTime 打起来也更短。

先前发布的声明关于nanoTime通常只具有微秒精度是正确的。此外,根据情况(其他方法也可能如此),它可能需要超过整个微秒才能调用,因此请不要期望正确计时非常非常小的时间间隔。


4
在多核环境中要谨慎使用nanoTime。 - jasonk
18
@entropy - 当然可以;基本上,在某些版本的操作系统/Java/CPU上,一些nanoTime的实现将使用硬件CPU时间戳计数器(TSC)。但是,这个TSC可能在不同的核心/处理器之间不同。如果您的线程在中途被重新调度到另一个核心,您将会得到来自核心1的起始时间戳和来自核心2的结束时间戳,但它们可能不是相同的时间(甚至可能出现负值)。以下是一些例子:https://dev59.com/anRB5IYBdhLWcg3wyqKo 这是一个很好的例子。 - jasonk
5
@Brett 是的,完全正确 - 使用它,但要注意其误导性回复。简单的方法是对大量运行结果取平均值。 - jasonk
39
不要这样做!甚至不要除以1000000(这个概念上至少是正确的)。始终使用库,例如JDK的“TimeUnit.NANOSECONDS.toMillis(value)”。 - Kevin Bourrillion
7
因为它毫无意义地容易出错(正如评论所说明的那样),而且更难阅读。 - Kevin Bourrillion
显示剩余10条评论

238

在Java中要实现这个,使用long类型就可以了。下面是如何测量时间的更多信息...

System.currentTimeMillis()

实现传统方法,确实需要使用System.currentTimeMillis()

long startTime = System.currentTimeMillis();
// ... do something ...
long estimatedTime = System.currentTimeMillis() - startTime;

o.a.c.l.t.StopWatch

注意,Commons Lang库中有一个StopWatch类可用于以毫秒为单位测量执行时间。它具有像split()suspend()resume()等方法,允许在执行的不同点上进行测量,这可能会更方便。请查看一下。

System.nanoTime()

如果您正在寻找精确度极高的经过时间测量,则可以使用System.nanoTime()。根据其javadoc:

long startTime = System.nanoTime();    
// ... the code being measured ...    
long estimatedTime = System.nanoTime() - startTime;

Jamon

另一种选择是使用JAMon,这是一个工具,可以收集位于start()和stop()方法之间的任何代码的统计数据(执行时间、命中次数、平均执行时间、最小值、最大值等)。以下是一个非常简单的例子:

import com.jamonapi.*;
...
Monitor mon=MonitorFactory.start("myFirstMonitor");
...Code Being Timed...
mon.stop();

请阅读 这篇文章 以了解Java性能调优。

使用AOP

最后,如果您不想在代码中添加这些度量(或者无法更改现有的代码),那么AOP将是一种完美的工具。我不会深入讨论它,但我至少想提一下。

下面是一个非常简单的AspectJ和JAMon方面示例(在此,点切入的短名称将用于JAMon监视器,因此调用thisJoinPoint.toShortString()):

public aspect MonitorAspect {
    pointcut monitor() : execution(* *.ClassToMonitor.methodToMonitor(..));

    Object arround() : monitor() {
        Monitor monitor = MonitorFactory.start(thisJoinPoint.toShortString());
        Object returnedObject = proceed();
        monitor.stop();
        return returnedObject;
    }
}

切入点定义可以轻松适应基于类名、包名、方法名或这些任意组合的任何方法进行监视。测量确实是AOP的完美应用案例。


42

你的新类:

public class TimeWatch {    
    long starts;

    public static TimeWatch start() {
        return new TimeWatch();
    }

    private TimeWatch() {
        reset();
    }

    public TimeWatch reset() {
        starts = System.currentTimeMillis();
        return this;
    }

    public long time() {
        long ends = System.currentTimeMillis();
        return ends - starts;
    }

    public long time(TimeUnit unit) {
        return unit.convert(time(), TimeUnit.MILLISECONDS);
    }
}

使用方法:

    TimeWatch watch = TimeWatch.start();
    // do something
    long passedTimeInMs = watch.time();
    long passedTimeInSeconds = watch.time(TimeUnit.SECONDS);
之后,经过的时间可以转换为你喜欢的任何格式,例如使用日历。

问候,GHad

2
+1 对于 TimeUnit 的提及。我不知道它的存在(为什么它不在 java.util 包中?) - OscarRyz
@Oscar,也许Lea先生考虑过它是并发API的一部分。 - Pascal Thivent
TimeUnit自Java 1.5以来就存在,但在Java 1.6中得到了扩展。它是一个非常有用的枚举类型,可以很好地使用。 - GHad
创建那个类的想法很棒。赞! - ecruz

23

简短概括

例如,如果startTime是23:00,endTime是1:00,则持续时间为2:00。

不可能。如果只有时间,那么时钟会在午夜停止。没有日期的上下文,我们怎么知道你是指下一天、下一周还是下一个十年的1 AM?

因此,从晚上11点到凌晨1点意味着时间向后倒退了22个小时,时钟的指针逆时针运转。请看下面的结果,是负二十二小时。

Duration.between(              // Represent a span of time a total number of seconds plus a fractional second in nanoseconds.
    LocalTime.of( 23 , 0 ) ,   // A time-of-day without a date and without a time zone. 
    LocalTime.of( 1 , 0 )      // A time-of-day clock stops at midnight. So getting to 1 AM from 11 PM means going backwards 22 hours.
)                              // Return a `Duration` object.
.toString()                    // Generate a `String` representing this span of time using standard ISO 8601 format: PnYnMnDTnHnMnS

PT-22H

跨越午夜需要加上日期的大背景,除了时间(见下文)。

如何在Java中测量经过的时间?

  1. 使用 Instant.now() 以UTC时区捕获当前时间。
  2. 稍后再次捕获另一个这样的时刻。
  3. 将两个时刻都传递给 Duration.between 方法。
  4. (a) 通过调用各种 to…Part 方法,从生成的 Duration 对象中提取一定数量的24小时天数、小时、分钟、秒和纳秒分数部分。
    (b) 或者,调用 toString 生成一个符合 标准ISO 8601格式PnYnMnDTnHnMnS 字符串。

示例代码,使用一对 Instant 对象。

Duration.between(    // Represent a span of time a total number of seconds plus a fractional second in nanoseconds.
    then ,           // Some other `Instant` object, captured earlier with `Instant.now()`.
    Instant.now()    // Capture the current moment in UTC with a resolution as fine as nanoseconds, depending on the limits of your host computer hardware clock and operating system. Generally you will get current moment in microseconds (six decimal digits of fractional second) in Java 9, 10, and 11, but only milliseconds in Java 8. 
)                    // Return a `Duration` object.
.toString()          // Generate a `String` representing this span of time using standard ISO 8601 format: PnYnMnDTnHnMnS

PT3M27.602197S

Java 8+中的新技术

现在,Java 8及其后续版本内置了新技术,即java.time框架。

java.time

java.time框架由JSR 310定义,灵感来自于非常成功的Joda-Time项目,由ThreeTen-Extra项目扩展,并在Oracle Tutorial中描述。

旧的日期时间类(例如java.util.Date/.Calendar)随Java最早版本捆绑在一起,已被证明设计不良,令人困惑和麻烦。它们被java.time类所取代。

分辨率

其他答案讨论了分辨率。

java.time类具有纳秒的分辨率,最多可达小数点后九位。例如:2016-03-12T04:29:39.123456789Z

旧的java.util.Date/.Calendar类和Joda-Time类都具有毫秒分辨率(3位小数)。例如,2016-03-12T04:29:39.123Z

在Java 8中,由于传统问题,当前时刻仅以毫秒分辨率获取。在Java 9及更高版本中,如果您的计算机硬件时钟运行得如此精细,则可以确定当前时间的纳秒分辨率。

时间-仅限于当天

如果您真正想只使用缺少任何日期或时区的时间,请使用LocalTime类。

LocalTime sooner = LocalTime.of ( 17, 00 );
LocalTime later = LocalTime.of ( 19, 00 );

Duration 表示一个时间跨度,以秒和纳秒为单位计数。

Duration duration = Duration.between ( sooner, later );

将内容输出到控制台。

System.out.println ( "sooner: " + sooner + " | later: " + later + " | duration: " + duration );

早于:17:00 | 晚于:19:00 | 持续时间:PT2H

ISO 8601

请注意,Duration::toString 的默认输出是使用标准 ISO 8601 格式。在这种格式中,P 表示开始(如“周期”),而 T 将任何年份-月份-日期部分与小时-分钟-秒钟部分分开。

跨越午夜

不幸的是,当您跨越午夜时只考虑一天内的时间会变得棘手。通过假设您想要回到一天内较早的时间点,LocalTime 类处理此问题。

使用上面相同的代码,但从 23:0001:00 结果为负二十二小时 (PT-22H)。

LocalTime sooner = LocalTime.of ( 23, 0 );
LocalTime later = LocalTime.of ( 1, 0 );

早于:23:00 | 晚于:01:00 | 持续时间:PT-22H

日期和时间

如果您打算跨越午夜,那么使用日期和时间值而不是仅有时间的值可能更有意义。

时区

时区对于日期非常重要。因此,我们指定三个项目:(1)所需日期,(2)所需时间,以及(3)作为解释该日期和时间的上下文的时区。在这里,我们任意选择了蒙特利尔地区的时区。

如果您仅通过偏移量定义日期,请使用ZoneOffsetOffsetDateTime。 如果您有一个完整的时区(偏移加上处理异常规则,例如夏令时),请使用ZoneIdZonedDateTime

LocalDate localDate = LocalDate.of ( 2016, 1, 23 );
ZoneId zoneId = ZoneId.of ( "America/Montreal" );
ZonedDateTime sooner = ZonedDateTime.of ( localDate, LocalTime.of ( 23, 0 ), zoneId );

我们将较晚的时间指定为第二天凌晨1:00。
ZonedDateTime later = ZonedDateTime.of ( localDate.plusDays ( 1 ), LocalTime.of ( 1, 0 ), zoneId );

我们按照上面所见的方式计算Duration。现在我们得到了这个问题所期望的两个小时。
Duration duration = Duration.between ( sooner, later );

输出到控制台。

System.out.println ( "sooner: " + sooner + " | later: " + later + " | duration: " + duration );

早:2016-01-23T23:00-05:00[美国/蒙特利尔] | 晚:2016-01-24T01:00-05:00[美国/蒙特利尔] | 持续时间:PT2H

夏令时

如果涉及到夏令时(DST)或其他类似异常的日期时间,java.time类将根据需要进行调整。请阅读类文档以了解详情。


关于 java.time

java.time 框架是内置于 Java 8 及更高版本中的。这些类取代了老旧且麻烦的 传统 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

Joda-Time项目现在处于维护模式,建议迁移到java.time类。

要了解更多信息,请参见Oracle教程。并搜索Stack Overflow以获取许多示例和解释。规范是JSR 310

您可以直接与数据库交换java.time对象。使用符合JDBC 4.2或更高版本的JDBC驱动程序。无需字符串,也不需要java.sql.*类。

如何获取java.time类?

ThreeTen-Extra 项目扩展了 java.time 的附加类。该项目是 java.time 可能未来添加的一个试验场。您可能会在这里找到一些有用的类,例如 Interval, YearWeek, YearQuarter更多


1
这个答案看起来大部分是复制粘贴自 https://dev59.com/qFwY5IYBdhLWcg3wl4sb#32443004,虽然它提到了时间,但并没有回答问题。 - Entea
@Entea 我建议您仔细重新阅读。至于复制粘贴,该链接的答案没有关于分辨率的部分,没有讨论“持续时间”,也没有解释白天节约时间被忽略的问题,这只是一个基于时间的计算。至于不在此处回答问题,我会说 Duration.between (sooner, later) 结合对跨越午夜问题的解释是对“如果 startTime 是 23:00,endTime 是 1:00,要获得 2:00 的持续时间”的完美回答。虽然我欣赏建设性的批评,但我期望更多的勤奋。 - Basil Bourque
我已经按照您的建议重新仔细阅读了答案。它确实回答了问题。我通过阅读前两三段并没有找到与问题有很多联系而过早地得出结论。那是我的错误,我为我的先前评论道歉。我想知道这里是否有一个建设性的评论:通过以被问问题本身开始重组答案的方式来改进答案。段落可以按抽象程度降序排列,也许从一般方式回答“如何测量经过的时间”,然后进入细节。 - Entea
@Entea 道歉已经被接受。根据您的评论,我在顶部添加了一个“tl;dr”部分,直接阐述了问题的要点,并提供了错误和正确计算经过时间的实用代码示例。这是对答案的很好改进。感谢您的建议。 - Basil Bourque
感谢您抽出时间回复我的评论。我学到了一个很好的教训,即一篇起初似乎含糊不清的论文不应被完全拒绝,因为它可能实际上包含有效的观点。这可能是人们在经历高效沟通的需要时必须学习的一般规则。 - Entea

23

如果你的目的只是为了在程序日志中打印粗略的时间信息,那么对于Java项目来说,简单的解决方案不是编写自己的秒表或定时器类,而是使用Apache Commons Lang提供的一部分——org.apache.commons.lang.time.StopWatch类。

final StopWatch stopwatch = new StopWatch();
stopwatch.start();
LOGGER.debug("Starting long calculations: {}", stopwatch);
...
LOGGER.debug("Time after key part of calcuation: {}", stopwatch);
...
LOGGER.debug("Finished calculating {}", stopwatch);

大多数记录器还支持时间戳作为日志输出的一部分。 - James McMahon
2
@James McMahon。是的,但这些时间戳显示的是当前时间,而不是给定任务的累计时间。 - David Bliss
1
请注意,StopWatch 在其实现中使用了 System.currentTimeMillis()。 - aerobiotic
System.currentTimeMillis() 仅适用于 getStartTime() 属性。时间计算完全基于 System.nanoTime()(至少在 org.apache.commons.lang3 版本中)。 - Ilya Serbis

8
值得注意的是,
  • System.currentTimeMillis() 函数具有毫秒级别的准确度,但在某些Windows系统上可能为16毫秒。它的成本低于其他替代方案 < 200 纳秒。
  • System.nanoTime() 函数在大多数系统上只有微秒级别的准确度,并且在Windows系统上可能会出现100微秒的跳变(即有时它并不像表面上那样准确)
  • Calendar 函数计算时间的成本非常高,(除了XMLGregorianCalendar外)。有时它是最合适的解决方案,但请注意仅应计时长时间间隔。

16ms和Windows有什么关系?Windows每16ms读取一次系统时钟? - Koray Tugay
@KorayTugay,旧版本的Windows使用的时钟以60 Hz的频率跳动,大约每16.7毫秒一次。我认为它被用来采样键盘上按下的键。 - Peter Lawrey

7

Java提供了静态方法System.currentTimeMillis()。它返回一个long值,因此是一个很好的参考。许多其他类也接受一个名为'timeInMillis'的参数,它也是长整型。

许多人发现使用Joda Time库来计算日期和时间更容易。


+1 提到了 Joda Time。将其移植到 Java 正确的位置发生了什么?JSR310? - blank
@Bedwyr:很不幸,它仍未完成。也许得等到Java8。 - BalusC
@BalusC 嗯,也许他们现在可以抽出时间进行这项工作了,因为闭包已经完成,一切都被推迟了。 - blank

6

在Java中要实现这个功能需要使用哪些类型?

答案: long

public class Stream {
    public long startTime;
    public long endTime;

    public long getDuration() {
        return endTime - startTime;
    }
    // I  would add
    public void start() {
        startTime = System.currentTimeMillis();
    }
    public void stop() {
         endTime = System.currentTimeMillis();
     }
}

使用方法:

  Stream s = .... 

  s.start();

  // do something for a while 

  s.stop();

  s.getDuration(); // gives the elapsed time in milliseconds. 

这是我对你的第一个问题的直接答案。

至于最后的“note”,我建议你使用Joda Time。它包含一个适合你需要的interval类。


System.currentTimeMillis() 返回当前时间和协调世界时1970年1月1日午夜之间的毫秒差。 :) 我想我可以接受这个;我担心如果开始是昨天,结束是今天会有问题。 - Omu
1
很高兴看到@ChuckNorris在使用StackOverFlow。 - Tim

3
Byte Stream Reader Elapsed Time for 23.7 MB is 96 secs

import java.io.*;
import java.io.IOException;
import java.util.Scanner;

class ElaspedTimetoCopyAFileUsingByteStream
{

    private long startTime = 0;
    private long stopTime = 0;
    private boolean running = false;


    public void start() 
    {
        this.startTime = System.currentTimeMillis();
        this.running = true;
    }


    public void stop() 
    {
        this.stopTime = System.currentTimeMillis();
        this.running = false;
    }



    public long getElapsedTime() 
    {
        long elapsed;
        if (running) {
             elapsed = (System.currentTimeMillis() - startTime);
        }
        else {
            elapsed = (stopTime - startTime);
        }
        return elapsed;
    }



    public long getElapsedTimeSecs()                 
    {
        long elapsed;
        if (running) 
        {
            elapsed = ((System.currentTimeMillis() - startTime) / 1000);
        }
        else
        {
            elapsed = ((stopTime - startTime) / 1000);
        }
        return elapsed;
    }





    public static void main(String[] args) throws IOException
    {
        ElaspedTimetoCopyAFileUsingByteStream  s = new ElaspedTimetoCopyAFileUsingByteStream();
        s.start();

        FileInputStream in = null;
        FileOutputStream out = null;

      try {
         in = new FileInputStream("vowels.txt");   // 23.7  MB File
         out = new FileOutputStream("output.txt");

         int c;
         while ((c = in.read()) != -1) {
            out.write(c);
         }
      }finally {
         if (in != null) {
            in.close();
         }
         if (out != null) {
            out.close();
         }
      }



        s.stop();
        System.out.println("elapsed time in seconds: " + s.getElapsedTimeSecs());
    }
}

[Elapsed Time for Byte Stream Reader][1]

**Character Stream Reader Elapsed Time for 23.7 MB is 3 secs**

import java.io.*;
import java.io.IOException;
import java.util.Scanner;

class ElaspedTimetoCopyAFileUsingCharacterStream
{

    private long startTime = 0;
    private long stopTime = 0;
    private boolean running = false;


    public void start() 
    {
        this.startTime = System.currentTimeMillis();
        this.running = true;
    }


    public void stop() 
    {
        this.stopTime = System.currentTimeMillis();
        this.running = false;
    }



    public long getElapsedTime() 
    {
        long elapsed;
        if (running) {
             elapsed = (System.currentTimeMillis() - startTime);
        }
        else {
            elapsed = (stopTime - startTime);
        }
        return elapsed;
    }



    public long getElapsedTimeSecs()                 
    {
        long elapsed;
        if (running) 
        {
            elapsed = ((System.currentTimeMillis() - startTime) / 1000);
        }
        else
        {
            elapsed = ((stopTime - startTime) / 1000);
        }
        return elapsed;
    }





    public static void main(String[] args) throws IOException
    {
        ElaspedTimetoCopyAFileUsingCharacterStream  s = new ElaspedTimetoCopyAFileUsingCharacterStream();
        s.start();

         FileReader in = null;                // CharacterStream Reader
      FileWriter out = null;

      try {
         in = new FileReader("vowels.txt");    // 23.7 MB
         out = new FileWriter("output.txt");

         int c;
         while ((c = in.read()) != -1) {
            out.write(c);
         }
      }finally {
         if (in != null) {
            in.close();
         }
         if (out != null) {
            out.close();
         }
      }

              s.stop();
        System.out.println("elapsed time in seconds: " + s.getElapsedTimeSecs());
    }
}


[Elapsed Time for Character Stream Reader][2]


  [1]: https://istack.dev59.com/hYo8y.webp
  [2]: https://istack.dev59.com/xPjCK.webp

3

如果你更喜欢使用Java的日历API,你可以尝试这个方法:

Date startingTime = Calendar.getInstance().getTime();
//later on
Date now = Calendar.getInstance().getTime();
long timeElapsed = now.getTime() - startingTime.getTime();

3
不好的想法。它增加了比毫秒时间更多的开销。如果你只需要毫秒级的时间,只需使用 System.currentTimeMillis(),你不需要知道时区和在 Calendar#toString()中看到的所有内容。请注意,Calendar类包含了更多的功能,可能会给你带来额外的复杂性和开销。 - BalusC
1
你说得有道理。我不知道这是否会对性能产生可衡量的影响,但这可能是一个好的实践。然而,如果你已经有了一个日历对象,或者想要进行更复杂的操作,那么使用Calendar可能是有效的。 - James McMahon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接