1. 程式人生 > >java中獲取比毫秒更為精確的時間

java中獲取比毫秒更為精確的時間

關鍵詞: java 毫秒 微秒 納秒 System.currentTimeMillis() 誤差

在對新寫的超快xml解析器和xpath引擎進行效率測試時,為獲取執行時間,開始也沒多想就用了System.currentTimeMillis() 來做的。由此碰到一個極其詭異的問題,同樣的程式碼迴圈執行數次,分析每一次的執行時間,發現一大部分執行時間為小於1毫秒,但其間也發現有相當一部分的執行時間有非常大的跳躍,而且時間都近似16毫秒左右。這個1毫秒和16毫秒結果,以計算機的執行速度看,差距是驚人的,必須找出其原因。

根據經驗,在16毫秒時間內,cpu可以運算的指令數量是相當驚人的,所以可以基本斷定這16ms的差距,應當不是cpu在執行指令,另外因為測試過程中gc輸出也已經開啟,未見gc發生,所以懷疑可能是發生了什麼io阻塞,比如檔案讀寫、載入類庫、或者什麼網路操作等,由於筆者測試的系統的環境比較複雜,其間有用到ehCache,資料庫操作等,排查起來非常不容易。

在困擾了好一陣之後,忽然想到可能計時系統有誤差,這才翻回來查了下System.currentTimeMillis() 的文件,原來這個方法呼叫了個native方法,獲取的時間精度會依賴於作業系統的實現機制。奶奶的!

既然不準,就看看有沒更準的方法,在jdk5原始碼中,挨著System.currentTimeMillis() 定義就是System.nanoTime() 方法,靠,一下來了個精準1000000倍的取納秒的方法,不過看jdk文件介紹,這個方法的精度依然依賴作業系統,不過再不準也能達到微秒級的準確度,放心用吧! 結果測試結果一下準確了不少,沒再發現比較大的跳躍了。

所以這裡提醒做非常精確的時間統計的朋友,謹慎使用System.currentTimeMillis() 。

雖然用取納秒的方法解決了我的問題,但對於為何使用System.currentTimeMillis() 會造成那麼大的跳躍,一直無解,有點鬱悶。幸運的是今天恰巧看到一個網友做的測試很有意思,用事實資料證明了這個跳躍的存在,分享給感興趣的同學!

以下內容為轉帖:

在Java中可以通過System.currentTimeMillis()或者System.nanoTime() (JDK>=5.0) 方法獲得當前的時間的精確值。但是通過閱讀Javadoc,我們發現這兩個方法並不一定保證得到你所期望的精度。先來看System.currentTimeMillis():

Returns the current time in milliseconds. Note that while the unit of time of the return value is a millisecond, the granularity of the value depends on the underlying operating system and may be larger. For example, many operating systems measure time in units of tens of milliseconds.

誠如上面所說返回值的粒度依賴於底層作業系統,那麼它在不同的平臺上到底能提供是麼樣的精度,是否像函式名所寫的那樣真正 精 確到1毫秒呢?看下面一段測試程式:


public class ClockAccuracyTest {

    public static void main(String args[]) {

        SimpleDateFormat formatter = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss:SSS");
        int size = 4000000;

        // create an array to hold millisecond times
        // and loop to capture them
        long times[] = new long[size];
        for (int i = 0; i < size; i++) {
            times[i] = System.currentTimeMillis();
        }

        // now display the unique times
        long time = times[0];
        long previousTime = times[0];
        long count = 0;
        Set<Long> deltas = new HashSet<Long>();       
        long delta = 0;
        long minDelta = Long.MAX_VALUE;
        long maxDelta = Long.MIN_VALUE;
        for (int i = 0; i < size; i++) {
            if (times[i] > time) {
                delta = time - previousTime;
                deltas.add(delta);
                if (delta > 0 && delta < minDelta) {
                    minDelta = delta;
                } else if (delta > maxDelta) {
                    maxDelta = delta;
                }

                System.out.print("raw=");
                System.out.print(time);
                System.out.print(" | formatted=");
                System.out.print(formatter.format(new Date(time)));
                System.out.print(" | frequency=");
                System.out.print(count);
                System.out.print(" | delta=");
                System.out.print(delta);
                System.out.println("ms");

                previousTime = time;
                time = times[i];
                count = 0;
            } else {
                count++;
            }
        }

        System.out.println("");
        System.out.println("Minimum delta : " + minDelta + "ms");
        System.out.println("Maximum delta : " + maxDelta + "ms");

    }

}


這段程式迴圈呼叫 System.currentTimeMillis()方法, 記錄並顯示結果,在我機器(Windows XP SP3)上執行輸出如下:

raw=1255659457187 | formatted=16-十月-2009 10:17:37:187 | frequency=147250 | delta=0ms
raw=1255659457203 | formatted=16-十月-2009 10:17:37:203 | frequency=447674 | delta=16ms
raw=1255659457218 | formatted=16-十月-2009 10:17:37:218 | frequency=436583 | delta=15ms
raw=1255659457234 | formatted=16-十月-2009 10:17:37:234 | frequency=439379 | delta=16ms
raw=1255659457250 | formatted=16-十月-2009 10:17:37:250 | frequency=426547 | delta=16ms
raw=1255659457265 | formatted=16-十月-2009 10:17:37:265 | frequency=447048 | delta=15ms
raw=1255659457281 | formatted=16-十月-2009 10:17:37:281 | frequency=459522 | delta=16ms
raw=1255659457296 | formatted=16-十月-2009 10:17:37:296 | frequency=414816 | delta=15ms
raw=1255659457312 | formatted=16-十月-2009 10:17:37:312 | frequency=458826 | delta=16ms

Minimum delta : 15ms
Maximum delta : 16ms

輸出的四列從左到右分別是原始的毫秒值、格式化的時間、每個值迴圈的次數、與上一個不同值的差。可以看到在Windows上 System.currentTimeMillis()方法並不能提供1ms的計時粒度,它的粒度為15~16ms,從網上的其它文章來看,這個結果基本上是一致的,這也驗證了Javadoc上所寫的“ the granularity of the value depends on the underlying operating system and may be larger ”。在其他作業系統,如Linux、Solaris上,我沒有進行測試,但從網上的一些測試結果看, currentTimeMillis方法在 某些作業系統能夠提供精確的1毫秒計時粒度。這是不是意味著Java在Windows上無法進行精確的毫秒計時了呢?當然不是,一種解決方案是採用JNI呼叫,利用Windows系統提供的函式計時;還有一個更簡便的辦法,就是使用JDK5.0加入的System.nanoTime()方法。Javadoc對該方法的描述如下:

Returns the current value of the most precise available system timer, in nanoseconds.

This method can only be used to measure elapsed time and is not related to any other notion of system or wall-clock time. The value returned represents nanoseconds since some fixed but arbitrary time (perhaps in the future, so values may be negative). This method provides nanosecond precision, but not necessarily nanosecond accuracy. No guarantees are made about how frequently values change. Differences in successive calls that span greater than approximately 292 years (263 nanoseconds) will not accurately compute elapsed time due to numerical overflow.

它返回系統能夠提供的最為精確的計時,以納秒(10億分之一秒)為單位,但並不保證納秒級精度。把上面的測試程式中黑體的一行改為“times[i] = System.nanoTime();”,並把所有的“ms”該成“ns”,迴圈次數改為10。執行輸出如下:

raw=8705892679376 | formatted=17-十一月-2245 23:31:19:376 | frequency=1 | delta=0ns
raw=8705892681053 | formatted=17-十一月-2245 23:31:21:053 | frequency=0 | delta=1677ns
raw=8705892682449 | formatted=17-十一月-2245 23:31:22:449 | frequency=0 | delta=1396ns
raw=8705892683846 | formatted=17-十一月-2245 23:31:23:846 | frequency=0 | delta=1397ns
raw=8705892685522 | formatted=17-十一月-2245 23:31:25:522 | frequency=0 | delta=1676ns
raw=8705892686919 | formatted=17-十一月-2245 23:31:26:919 | frequency=0 | delta=1397ns
raw=8705892688316 | formatted=17-十一月-2245 23:31:28:316 | frequency=0 | delta=1397ns
raw=8705892689713 | formatted=17-十一月-2245 23:31:29:713 | frequency=0 | delta=1397ns
raw=8705892691110 | formatted=17-十一月-2245 23:31:31:110 | frequency=0 | delta=1397ns

Minimum delta : 1396ns
Maximum delta : 1676ns

我們看到frequency=0,這意味著每執行一次迴圈,nanoTime方法的返回值都發生了改變,改變的差值平均大約為1467ns(不同效能的機器結果會不同)。這說明nanoTime方法計時的精度是非常高的,粒度比方法本身的呼叫執行耗時還要小。

回到上面的問題,如何在windows上實現精確的毫秒計時呢。答案就是用“System.nanoTime()/1000000L”代替“System.currentTimeInMills()”。把測試程式黑體的一行程式碼改為"times[i] = System.nanoTime()/1000000L;",迴圈次數5000,執行輸出結果如下:

raw=9487129 | formatted=01-一月-1970 10:38:07:129 | frequency=202 | delta=0ms
raw=9487130 | formatted=01-一月-1970 10:38:07:130 | frequency=704 | delta=1ms
raw=9487131 | formatted=01-一月-1970 10:38:07:131 | frequency=621 | delta=1ms
raw=9487132 | formatted=01-一月-1970 10:38:07:132 | frequency=618 | delta=1ms
raw=9487133 | formatted=01-一月-1970 10:38:07:133 | frequency=696 | delta=1ms
raw=9487134 | formatted=01-一月-1970 10:38:07:134 | frequency=695 | delta=1ms
raw=9487135 | formatted=01-一月-1970 10:38:07:135 | frequency=698 | delta=1ms
raw=9487136 | formatted=01-一月-1970 10:38:07:136 | frequency=698 | delta=1ms

Minimum delta : 1ms
Maximum delta : 1ms

我們看到粒度delta變為了1ms。迴圈次數frequency平均為676,也就是說每個迴圈運執行耗時1/676=0.001479ms=1479ns,與上一個測試結果的1467ns吻合。

結論:如果你的Java程式需要高精度的計時,如1毫秒或者更小,使用System.nanoTime()方法,它完全可以滿足你的需求。如果不行的話,建議你換臺更快的機器試試:)