System.currentTimeMillis()性能分析

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%

数据说明:

  1. 极端情况下1ms调用1次,8核CPU消耗点大概在2~5%左右。

  2. 调用频率为100ms时,CPU基本在1%左右。

  3. 数据说明单单一个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。

以下是系统总体性能的图表(可在一秒钟内在所有内核和处理器上执行的调用次数):

image

可以看出,从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);  
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容