共计 10833 个字符,预计需要花费 28 分钟才能阅读完成。
System.currentTimeMillis()性能问题的钻研与测试
1. 起因
明天工作做完了没事干在群里发了张吉冈里帆的比基尼写真图,狗治理说我发黄图禁我言,把我 Q 群摸鱼的权力都剥夺了,那行吧,刚好把之前测试过的一个货色整顿一下记录起来。
之前学雪花算法的时候看到很多示例会将 System.currentTimeMillis()用一个工具类缓存起来再由定时线程去保护,说是 System.currentTimeMillis()在高并发下会有性能问题,到底啥问题也没说分明。按理说 Java 库中获取低精度工夫最根本最罕用的办法:System.currentTimeMillis() 还能有啥问题啊,有问题早就炸了还能等到当初吗,所以正好着手测试一下 System.currentTimeMillis()的性能和网上的缓存工夫戳的方法到底靠不靠谱。
先说论断:然而事实如同真的就是如此,System.currentTimeMillis()这个办法在特定条件下的确可能存在重大的性能问题,然而缓存工夫戳的做法问题更大。
2. 剖析
通过测试发现,在 Windows 下该办法性能体现优良,在 Linux 系的操作系统中用 TSC、JVM_CLOCK 这些更高 rating 的时钟源性能也没有问题,然而如果是应用 HPET、ACPI_PM 这类高精度时钟源的时候,高并发下该办法的调用开销显著变高,曾经达到了不可承受的境地了(100 倍以上的差距)。翻进去咱们能够看到 System.currentTimeMillis()是一个 native 办法,象征这个办法的实现取决于异构语言的实现,可能会因为操作系统和装置的 JDK 不同从而有不同的实现,这也就是解释了为什么在特定条件下会有不同的性能体现,因为不同零碎的底层实现不一样嘛。
Windows 上的黑魔法实现性能十分好,缓存工夫戳的办法跟原生比简直能够说是没有性能劣势,然而 Windows 的分辨率也较低只有 0.5ms(这也齐全足够用的,毕竟 System.currentTimeMillis()自身也只是个低精度工夫获取的办法);而 Linux 上的实现比较复杂,会和零碎 API 打交道开销会比拟高,更者如果应用的是 HPET 这种整个零碎共享的工夫源,他的 API 调用在整个计算机上只有不到 200 万次 /s,加上别的开销,差距就会很显著了。
3. 测试过程
通过记录循环调用 5000 万次 System.currentTimeMillis()计算耗费。
public long testSystemMilliSpeed() {
long sum = 0;
long t1 = System.currentTimeMillis();
for (int i = 0; i < 50000000; i++) {sum += System.currentTimeMillis();
}
long t2 = System.currentTimeMillis();
System.out.println("[System.currentTimeMillis()] Sum =" + sum + "; time spent =" + (t2 - t1) +
"ms; or" + (t2 - t1) * 1.0E6 / 50000000 + "ns / iter");
return sum;
}
测试机器是阿里云计算优化型 C6 服务器,8 核 16GB,应用 AdoptOpenJDK1.8
零碎应用 WindowsServer 2019 和 CentOS 8.2
典型后果示例 Windows
单线程:
[System.currentTimeMillis()] Sum = 7016145501759210272; time spent = 181 ms; or 3.62 ns / iter
[System.currentTimeMillis()] Sum = 7016145521514297836; time spent = 173 ms; or 3.46 ns / iter
[System.currentTimeMillis()] Sum = 7016145533408438385; time spent = 189 ms; or 3.78 ns / iter
8 线程:
[System.currentTimeMillis()] Sum = 7014845600130793472; time spent = 312 ms; or 6.24 ns / iter
[System.currentTimeMillis()] Sum = 7014845600415181616; time spent = 316 ms; or 6.32 ns / iter
[System.currentTimeMillis()] Sum = 7014845600415181616; time spent = 314 ms; or 6.28 ns / iter
16 线程
[System.currentTimeMillis()] Sum = 7014831913525842714; time spent = 401 ms; or 8.02 ns / iter
[System.currentTimeMillis()] Sum = 7014831913438052239; time spent = 418 ms; or 8.36 ns / iter
[System.currentTimeMillis()] Sum = 7014831914892399458; time spent = 381 ms; or 7.62 ns / iter
小结
从输入后果能够看出,在 Windows 下 System.currentTimeMillis()的调用开销非常低,每次调用开销只有 3.62ns,约等于 27 亿次每秒,齐全能够随性所欲的在任何场景应用。在 4 个线程的时候甚至性能简直没有降落,达到了比单线程快约 4 倍的 92 亿次每秒,不过再持续加线程的时候单次调用开销就开始变高了,不过性能劣化的并不是很显著,调用的开销能够说是能够齐全忽略不计放心使用这个办法。还有就是 System.currentTimeMillis()在 Windows 上的黑魔法实现的代价是分辨率只有 0.5ms,他的精度绝对于 Linux 的较低的,不过这自身就不是高精度工夫的办法,这一点忽略不计算是 Windows 上一个优良的中央。
典型后果示例 Linux(TSC)
单线程
[System.currentTimeMillis()] Sum = 7014846681218501008; time spent = 1308 ms; or 26.16 ns / iter
[System.currentTimeMillis()] Sum = 7014846746587342078; time spent = 1307 ms; or 26.14 ns / iter
[System.currentTimeMillis()] Sum = 7014846811970028463; time spent = 1308 ms; or 26.16 ns / iter
8 线程
[System.currentTimeMillis()] Sum = 7014815771421781278; time spent = 1563 ms; or 31.26 ns / iter
[System.currentTimeMillis()] Sum = 7014815771870699367; time spent = 1561 ms; or 31.22 ns / iter
[System.currentTimeMillis()] Sum = 7014815774818805007; time spent = 1588 ms; or 31.76 ns / iter
16 线程
[System.currentTimeMillis()] Sum = 7020243696839914036; time spent = 3147 ms; or 62.94 ns / iter
[System.currentTimeMillis()] Sum = 7020243692320997645; time spent = 3164 ms; or 63.28 ns / iter
[System.currentTimeMillis()] Sum = 7020243700477529928; time spent = 3069 ms; or 61.38 ns / iter
小结
从后果能够显著看出,在应用 TSC 时 Linux 上的确是要比 Windows 显著慢的(该死,Java 很多时候就是部署在 Linux 服务器上 …),单线程 26ns 约 3800 万次每秒,跑满 8 线程之后随线程数的减少开销稳固上身,开 4 倍的 64 线程单次开销同样也是稳固的减少 4 倍达到 120+ns 每次,这样的调用开销是齐全能够承受的,性能还是足够优良能够齐全安心的随便应用。
典型后果示例 Linux(ACPI_PM)
单线程
[System.currentTimeMillis()] Sum = 7020093683684623170; time spent = 168303 ms; or 3366.06 ns / iter
[System.currentTimeMillis()] Sum = 7020102078358700323; time spent = 167509 ms; or 3350.18 ns / iter
[System.currentTimeMillis()] Sum = 7020110475654659673; time spent = 168367 ms; or 3367.34 ns / iter
8 线程
[System.currentTimeMillis()] Sum = 7020137245340372492; time spent = 348907 ms; or 6978.14 ns / iter
[System.currentTimeMillis()] Sum = 7020137258588772567; time spent = 349399 ms; or 6987.98 ns / iter
[System.currentTimeMillis()] Sum = 7020137296734841559; time spent = 351311 ms; or 7026.22 ns / iter
16 线程
[System.currentTimeMillis()] Sum = 7020205171447447339; time spent = 751769 ms; or 15035.38 ns / iter
[System.currentTimeMillis()] Sum = 7020205162872284276; time spent = 751808 ms; or 15036.16 ns / iter
[System.currentTimeMillis()] Sum = 7020205146376911145; time spent = 751996 ms; or 15039.92 ns / iter
后果小结
注:Linux 中工夫源的分级,HPET、ACPI_PM 属于较低的“可用”级别,TSC 属于“优良”的时钟源,KVM_CLOCK 属于“现实”的工夫源
因为阿里云 ECS 上没有注册 HPET 工夫源,边应用了同级别的 ACPI_PM 工夫源同样达到了预期的后果,切换成 ACPI_PM 工夫源之后测试的后果记录好转我甚至没有方法跑完 JMH 测试(预计一天都跑不完),间接简略的运行几次获取一些典型后果。
结果显示应用 ACPI_PM 时性能连 Windows 的零头都不到,翻了足足一千倍只有约 30 万次每秒,曾经达到了微秒级别的单次调用开销,这样微小的调用开销可不能被疏忽掉,如果我的项目中刚好有大量应用 currentTimeMillis()这个办法,比如说统计每次申请耗时等高频场景则很有可能间接影响到我的项目的响应工夫。
4. JMH 测试
注:因为 ACPI_PM 太慢了所以没有方法跑完 JMH 测试,故只有 Linux(TSC)和 Windows 的测试后果
测试环境应用阿里云最新一代 c6 计算优化型 ECS,配置为 8 核 16G,JDK 应用 AdoptOpenJDK1.8_u282,操作系统别离是 WindowsServer2019 数据中心版和 CentOS 8.2
采纳下面的示例代码的循环调用形式,每次循环 5 千万次,别离测试单线程、4、8、16、32、64 线程并记录后果统计成表
结果显示在并发线程数小于服务器外围数时调用开销减少的并不显著,当并发大于等于服务器 CPU 外围数的时候体现一九比较稳定,随着并发的进步耗费工夫稳固减少,currentTimeMillis()这属于 CPU 密集型的办法,随着 CPU 满载和并发争抢性能达到饱理论 QPS 反而降落,属于预期后果。
如图,在应用 ACPI_PM 作为时钟源的时候速度切实是太慢了,开 16 线程的时候性能曾经降到了均匀 16 微秒能力执行一次调用,切实无奈保持等到实现测试
注:测试代码和后果在底部附录
5. 缓存工夫戳
下面提到了我做这次测试的起因,因为看到有些文章、工具包中采纳了缓存工夫戳代替原生 currentTimeMillis()的形式防止了可能的因为不同操作系统带来的不稳固因素,然而这个缓存工夫戳的形式靠不靠谱呢?所以同样也得再测试一遍能力下结论。
这是缓存工夫戳实现代码:
public class SystemClock {
private final AtomicLong now;
private static ScheduledExecutorService scheduler;
static {Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {if (scheduler != null) {scheduler.shutdownNow();
}
}
});
}
private SystemClock() {this.now = new AtomicLong(System.currentTimeMillis());
scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {Thread thread = new Thread(runnable, "SystemClockScheduled");
thread.setDaemon(true);
return thread;
});
scheduler.scheduleAtFixedRate(() -> now.set(System.currentTimeMillis()), 1, 1, TimeUnit.MILLISECONDS);
}
public static long now() {return getInstance().now.get();}
private enum SystemClockEnum {
SYSTEM_CLOCK;
private final SystemClock systemClock;
SystemClockEnum() {systemClock = new SystemClock();
}
public SystemClock getInstance() {return systemClock;}
}
private static SystemClock getInstance() {return SystemClockEnum.SYSTEM_CLOCK.getInstance();
}
}
吞吐量测试方法和下面 System.currentTimeMillis()的测试方法统一,循环调用 5 千万次,多线程用 JMH 进行测试
典型后果示例
单线程
## Windows
[BufferClock] Sum = 7014869507792291096; time spent = 16ms; or 0.32 ns / iter
[BufferClock] Sum = 7014869509083652400; time spent = 32ms; or 0.64 ns / iter
[BufferClock] Sum = 7014869510255266345; time spent = 31ms; or 0.62 ns / iter
## Linux(TSC)[BufferClock] Sum = 7014836520628701408; time spent = 25ms; or 0.5 ns / iter
[BufferClock] Sum = 7014836521867072124; time spent = 25ms; or 0.5 ns / iter
[BufferClock] Sum = 7014836523105825292; time spent = 25ms; or 0.5 ns / iter
## Linux(ACPI_PM)[BufferClock] Sum = 7014970447687570464; time spent = 25ms; or 0.5 ns / iter
[BufferClock] Sum = 7014970448949515660; time spent = 25ms; or 0.5 ns / iter
[BufferClock] Sum = 7014970450209997473; time spent = 25ms; or 0.5 ns / iter
8 线程
## Windows
[BufferClock] Sum = 7014837658100506766; time spent = 53ms; or 1.06 ns / iter
[BufferClock] Sum = 7014837658094441788; time spent = 53ms; or 1.06 ns / iter
[BufferClock] Sum = 7014837658032598985; time spent = 56ms; or 1.12 ns / iter
## Linux(TSC)[BufferClock] Sum = 7020249783930639245; time spent = 51ms; or 1.02 ns / iter
[BufferClock] Sum = 7020249784793206406; time spent = 51ms; or 1.02 ns / iter
[BufferClock] Sum = 7020249784998169933; time spent = 51ms; or 1.02 ns / iter
## Linux(ACPI_PM)[BufferClock] Sum = 7020287669118903216; time spent = 71ms; or 1.42 ns / iter
[BufferClock] Sum = 7020287669246604147; time spent = 57ms; or 1.14 ns / iter
[BufferClock] Sum = 7020287669311506737; time spent = 64ms; or 1.28 ns / iter
16 线程
## Windows
[BufferClock] Sum = 7014887357711793536; time spent = 52ms; or 1.04 ns / iter
[BufferClock] Sum = 7014887357711793536; time spent = 53ms; or 1.06 ns / iter
[BufferClock] Sum = 7014887357711793536; time spent = 54ms; or 1.08 ns / iter
## Linux(TSC)[BufferClock] Sum = 7020240799851649690; time spent = 101ms; or 2.02 ns / iter
[BufferClock] Sum = 7020240798802739189; time spent = 114ms; or 2.28 ns / iter
[BufferClock] Sum = 7020240798701704537; time spent = 118ms; or 2.36 ns / iter
## Linux(ACPI_PM)[BufferClock] Sum = 7020340267051437045; time spent = 192ms; or 3.84 ns / iter
[BufferClock] Sum = 7020340268861047443; time spent = 144ms; or 2.88 ns / iter
[BufferClock] Sum = 7020340269307897194; time spent = 183ms; or 3.66 ns / iter
JMH 测试后果
这次将 currentTimeMillis()的后果和缓存工夫戳的测试后果合并再了一起,从后果上看,在线程数超过外围数的时候 Linux 的斜率要比 Windows 上高一点,而绿色的和蓝色的缓存工夫戳线条齐全重合到了一起所以只能看到一条线。
局部过程截图
后果小结
从典型后果中能够看出,无论是在 Linux 还是 Windows 体现都非常稳固优良,属于齐全没有开销的办法,由此能够看出缓存工夫戳的办法的确可能无效的防止因为工夫源导致的迟缓问题。
6. 残暴事实
缓存工夫戳性能这么好,那咱们是不是能够间接应用它来代替 JDK 的办法了呢?后果是残暴的,缓存工夫戳的方法精度不现实也不可控,不倡议在任何中央应用(除非你对并发性能有极高要求然而又不在意精度,事实上这种假如不存在)
稳定性测试
一个获取工夫的办法除了要求快,还有个必要前提是它还要精确,如果后果都不精确,那再快都没有任何意义(我心算特快,2352 + 135212 = 97652,啪,我就问你快不快)
测试方法
通过记录一段时间内的调用后果,计算每次 ticks 距离的 ticks 的离散水平
注:因须要一个 5 亿长度的超大的 long 类型数组记录调用后果,所以至多须要给 JVM 调配 4GB 内存启动
public class TicksTest {
public static volatile boolean flag = true;
public static void main(String[] args) {TicksTest.step();
}
public static void step() {
// 5 亿
long[] values = new long[500000000];
// 实现定时 500ms
new Thread(() -> {
try {Thread.sleep(500L);
} catch (InterruptedException e) {e.printStackTrace();
}
TicksTest.flag = false;
}).start();
int length = 0;
long star = System.currentTimeMillis();
for (int i = 0; flag && i < values.length; i++) {values[i] = System.currentTimeMillis();
// values[i] = SystemClock.now();
length = i;
}
long end = System.currentTimeMillis();
// 统计后果
long min = 0, max = 0, total = 0, minCount = 0, maxCount = 0, totalCount = 0, last = 0, skip = 0;
List<Long> ticksList = new ArrayList<>(600);
List<Long> ticksCountList = new ArrayList<>(600);
for (int i = 1; i < length; i++) {if (values[i] != values[i - 1]) {long step = (values[i] - values[i - 1]);
long stepCount = i - last;
last = i;
System.out.printf("step %09d %d %d%n", i, step, stepCount);
// 跳过第一条
if (skip++ > 0) {
total += step;
totalCount += stepCount;
ticksList.add(step);
ticksCountList.add(stepCount);
if (max < step)
max = step;
if (min == 0 || min > step)
min = step;
if (maxCount < stepCount)
maxCount = stepCount;
if (minCount == 0 || minCount > stepCount)
minCount = stepCount;
}
}
}
System.out.printf("time: %d, count: %d," +
"ticks: (avg: %d, min: %d, max: %d)" +
"ticksCount: (avg: %d, min: %d, max: %d)",
end - star, ticksList.size(),
total / ticksList.size(), min, max,
totalCount / ticksCountList.size(), minCount, maxCount);
}
}
典型后果示例
这个测试后果是用我本人的开发电脑测试的,配置比拟低只有 4 核 16G
currentTimeMillis()
time: 515, count: 512, ticks: (avg: 1, min: 1, max: 1) ticksCount: (avg: 193040, min: 11356, max: 300382)
time: 509, count: 506, ticks: (avg: 1, min: 1, max: 1) ticksCount: (avg: 207048, min: 13123, max: 332162)
time: 513, count: 511, ticks: (avg: 1, min: 1, max: 1) ticksCount: (avg: 208990, min: 3133, max: 330624)
缓存工夫戳
time: 506, count: 39, ticks: (avg: 12, min: 1, max: 17) ticksCount: (avg: 7673476, min: 62472, max: 11029922)
time: 509, count: 36, ticks: (avg: 13, min: 1, max: 21) ticksCount: (avg: 8133013, min: 22302, max: 13444986)
time: 503, count: 36, ticks: (avg: 13, min: 1, max: 20) ticksCount: (avg: 8624468, min: 48840, max: 13364882)
由后果能够看出,JDK 的获取工夫戳办法每次 ticks 都稳固的是 1ms,间断屡次测试后果统一;而缓存工夫戳而后由异步线程每毫秒更新保护的办法 ticks 非常的不稳固,极少数 ticks 能失常的是 1ms,其余大多数工夫都是两位数以上的 ticks,真要到了极其的状况给你来个事实中隔 20ms 调用两次后果都是一样的,你的表情可能要解体了;
还有须要再次强调,currentTimeMillis()的后果取决于底层操作系统,个别操作系统可能是几十毫秒的 ticks,这个在 JDK 办法的正文上曾经有阐明了,然而目前来看咱们罕用 CentOS、Ubuntu、Depin 后果都是足够牢靠的。
7. 总结
在绝大多数状况下 JDK 的实现都比绝大多数的人的脑洞实现要牢靠的多,请置信 JDK,永远不要自作聪明,如果你要耍点小聪明,那么请确保通过详尽牢靠的测试验证!
8. 附录
测试代码和后果
https://github.com/yyyyyzc/cu…