System.currentTimeMillis()在java中是最常用的获取系统时间的方法,它返回的是1970年1月1日0点到现在经过的毫秒数。
在系统性能优化的过程中,定位问题的过程发现它似乎有较大性能损耗,所以本文对System.currentTimeMillis()做性能分析。
一、测试场景
在两个配置不同和操作系统不同的linux系统上分别单线程测试调用频率为1ms,100ms的情况,查看对cpu的性能损耗。测试环境比较干净,测试代码为简单的for循环调用。
对比结果如下:
机器配置 | 操作系统 | 调用频率 | CPU使用率 |
---|---|---|---|
8核CPU | Debian 2.6.26-29 | 1ms | 2%~5% |
1核VCPU | Debian 2.6.32-41 | 1ms | 2% |
8核CPU | Debian 2.6.26-29 | 100ms | 1% |
1核VCPU | Debian 2.6.32-41 | 100ms | 1% |
数据说明:
极端情况下1ms调用1次,8核CPU消耗点大概在2~5%左右。
调用频率为100ms时,CPU基本在1%左右。
数据说明单单一个System.currentTimeMillis()高频率调用还是有一定CPU消耗的。对一个毫秒级的接口来说这个性能损耗不算小。
所以在高并发的接口中还是应该尽量避免高频调用。
二、原因分析
针对System.currentTimeMillis()性能不好的原因分析,有一篇很好的文章The slow currentTimeMillis(),它直接从系统级、源码、汇编语言各个层次全方位的分析。
从The slow currentTimeMillis()中我们了解到,执行速度缓慢currentTimeMillis()是由两个因素造成的:
- JVM使用gettimeofday()而不是clock_gettime()
- gettimeofday() 如果使用HPET时间源,则速度非常慢。
但是,HPET现在不是唯一的时间源。最常见的时间源且许多系统使用的是TSC。在我们的项目中,服务器配置了HPET时间源,原因在于:此时间源与NTP客户端完美集成,可以平滑调整时间,而TSC不太稳定(我不知道细节;这是本地Linux大师所说的,我别无选择,只能相信他们)。其他一些开发人员可能会遇到同样的情况。此外,Java开发人员无法知道程序将在何种时间运行。
然而,如果我们使用TSC时间源,那么了解结果如何改变仍然很有趣。TSC代表时间戳记计数器,它仅仅是自启动以来计算的CPU周期数(它只有64位宽,因此它将在2.4GHz时钟频率下在243年内回绕)。该值可以使用rdtsc指令读取。传统上,这个值有两个问题:
- 来自不同内核或物理处理器的值可能相互移位,因为处理器可能在不同的时间开始
- 处理器的时钟频率可能会在执行期间发生变化。
第一个似乎确实是一个问题。我尝试rdtsc从多个内核中立即获取值,并在写入某个内存位置时同步。即使在最好的情况下,我也有几千个周期的差异。有时候更多。但是,如果程序员想要手动使用TSC,这只是一个问题; 在这种情况下,必须相应地设置线程关联。操作系统知道它何时重新调度从一个核心到另一个核心的线程,因此它可以进行所有必要的调整。
第二个问题似乎已成为过去。英特尔文档说:
处理器系列以不同的方式增加时间戳计数器:
- 对于Pentium M处理器(系列[06H],型号[09H,0DH]); 对于奔腾4处理器,英特尔至强处理器(系列[0FH],型号[00H,01H或02H]); 对于P6系列处理器:时间戳计数器随着每个内部处理器时钟周期递增。内部处理器时钟周期由当前内核时钟与总线时钟比决定。英特尔®SpeedStep®技术转换也可能影响处理器时钟。
- 对于奔腾4处理器,英特尔至强处理器(系列[0FH],型号[03H和更高]); 英特尔Core Solo和英特尔酷睿双核处理器(系列[06H],型号[0EH]); 英特尔至强处理器5100系列和英特尔酷睿2双核处理器(系列[06H],型号[0FH]); 用于英特尔酷睿2和英特尔至强处理器(家族[06H],DisplayModel [17H]); 对于Intel Atom处理器(系列[06H],DisplayModel [1CH]):时间戳计数器以恒定速率递增。
它还说:
处理器对不变TSC的支持由CPUID.80000007H:EDX [8]表示。
三、多线程下使用System.currentTimeMillis()
在currentTimeMillis()
基于HPET为640ns(1.5M operations/second)运行。这是每个核心还是整个系统?让我们运行一个类似的测试Time.java
,但启动N个线程,其中N在1到24之间(包括双处理器系统中的核心总数)。
以下是少量线程的结果:
线程数 | 平均时间/访问次数,ns | 总访问次数/秒,mil |
---|---|---|
1 | 644 | 1.55 |
2 | 918 | 2.18 |
3 | 1366 | 2.20 |
4 | 1871 | 2.14 |
以下是执行currentTimeMillis()
所有线程计数所需的平均时间:
这看起来非常线性,这让我们怀疑HPET芯片串行化请求,一次只能服务一个。或者,从线程计数1到线程计数2的转换,性能没有减半,而是下降了1.5倍,可能略高于1。
以下是系统总体性能的图表(可在一秒钟内在所有内核和处理器上执行的调用次数):
可以看出,从1.5M上升到大约2.1M op/sec,并在那里停留。最初的增长可能与我们在双处理器系统上进行测试有关。以下是执行限于单处理器(taskset 0x555
)时测得的时间:
线程数 | 平均时间/访问次数,ns,双处理器 | 平均时间/访问次数,ns,单处理器 |
---|---|---|
1 | 644 | 596 |
2 | 918 | 1105 |
3 | 1366 | 1672 |
4 | 1871 | 2245 |
单处理器时间不显示一个和两个线程之间的异常步骤; 它大致与线程数成比例,并且(除了一个线程的值)比双处理器时间长。
多进程测试给出了与多线程相似的结果。
简而言之,HPET的性能确实在系统范围内有限。无论我们如何分配核心和流程之间的负载,每秒钟不超过两百万次的查询时间可以在机器上执行。如果24个内核均匀加载,每个内核每秒可以执行低于100K的操作。这意味着使用时必须还真要小心,
currentTimeMillis()
在Java程序。
一个侧面说明。由于处理器在使用HPET时会相互影响,因此存在潜在的安全问题。一个进程可能会执行紧密循环调用
gettimeofday
,从而导致所有其他进程访问此资源并降低其性能。或者,某些进程可能调用此函数并使用TSC执行其执行时间,当其他进程查询当前时间时检测此方式,这可能有助于确定其他进程执行的执行路径。
基于TSC的计时器不存在此行为。它的性能非常稳定,只有在使用所有内核(包括超线程内核)时才会降低40%
四、解决方案
那有什么策略既可以获取系统时间又避免高频率调用呢。
- 策略一:如果对时间精确度要求不高的话可以使用独立线程缓存时间戳。
- 策略二:使用Linux的clock_gettime()方法。
Java VM可以使用这个调用并且提供更快的速度currentTimeMillis()。
如果绝对必要,可以使用JNI自己实现它.
- 策略三:使用System.nanoTime()。
策略一实现代码:
class MillisecondClock {
private long rate = 0;// 频率
private volatile long now = 0;// 当前时间
private MillisecondClock(long rate) {
this.rate = rate;
this.now = System.currentTimeMillis();
start();
}
private void start() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(rate);
} catch (InterruptedException e) {
e.printStackTrace();
}
now = System.currentTimeMillis();
}
}).start();
}
public long now() {
return now;
}
public static final MillisecondClock CLOCK = new MillisecondClock(10);