我只是将模块从旧的Java日期迁移到新的java.time API,并注意到性能大幅下降。它归结为具有时区的日期解析(我一次解析数百万个日期)。
没有时区(yyyy/MM/dd HH:mm:ss)的日期字符串的解析速度很快-比旧的Java日期快约2倍,在我的PC上每秒约有150万次操作。
yyyy/MM/dd HH:mm:ss
但是,当模式包含时区(yyyy/MM/dd HH:mm:ss z)时,使用新java.timeAPI 的性能下降约15倍,而使用旧API 的性能大约与没有时区的性能一样。请参阅下面的性能基准。
yyyy/MM/dd HH:mm:ss z
java.time
是否有人可以使用新的java.timeAPI 快速解析这些字符串?目前,作为一种解决方法,我正在使用旧的API进行解析,然后将转换Date为Instant,这并不是特别好。
Date
import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OperationsPerInvocation; import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; @OutputTimeUnit(TimeUnit.MILLISECONDS) @BenchmarkMode(Mode.AverageTime) @OperationsPerInvocation(1) @Fork(1) @Warmup(iterations = 3) @Measurement(iterations = 5) @State(Scope.Thread) public class DateParsingBenchmark { private final int iterations = 100000; @Benchmark public void oldFormat_noZone(Blackhole bh, DateParsingBenchmark st) throws ParseException { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); for(int i=0; i<iterations; i++) { bh.consume(simpleDateFormat.parse("2000/12/12 12:12:12")); } } @Benchmark public void oldFormat_withZone(Blackhole bh, DateParsingBenchmark st) throws ParseException { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss z"); for(int i=0; i<iterations; i++) { bh.consume(simpleDateFormat.parse("2000/12/12 12:12:12 CET")); } } @Benchmark public void newFormat_noZone(Blackhole bh, DateParsingBenchmark st) { DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder() .appendPattern("yyyy/MM/dd HH:mm:ss").toFormatter(); for(int i=0; i<iterations; i++) { bh.consume(dateTimeFormatter.parse("2000/12/12 12:12:12")); } } @Benchmark public void newFormat_withZone(Blackhole bh, DateParsingBenchmark st) { DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder() .appendPattern("yyyy/MM/dd HH:mm:ss z").toFormatter(); for(int i=0; i<iterations; i++) { bh.consume(dateTimeFormatter.parse("2000/12/12 12:12:12 CET")); } } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder().include(DateParsingBenchmark.class.getSimpleName()).build(); new Runner(opt).run(); } }
10万次运算的结果:
Benchmark Mode Cnt Score Error Units DateParsingBenchmark.newFormat_noZone avgt 5 61.165 ± 11.173 ms/op DateParsingBenchmark.newFormat_withZone avgt 5 1662.370 ± 191.013 ms/op DateParsingBenchmark.oldFormat_noZone avgt 5 93.317 ± 29.307 ms/op DateParsingBenchmark.oldFormat_withZone avgt 5 107.247 ± 24.322 ms/op
更新:
我只是对java.time类进行了分析,实际上,时区解析器似乎效率很低。仅解析一个独立的时区是造成所有缓慢的原因。
@Benchmark public void newFormat_zoneOnly(Blackhole bh, DateParsingBenchmark st) { DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder() .appendPattern("z").toFormatter(); for(int i=0; i<iterations; i++) { bh.consume(dateTimeFormatter.parse("CET")); } }
有一个名为类ZoneTextPrinterParser的java.time捆绑,这是内部制作组在每一个所有可用时区的副本parse()呼叫(通过ZoneRulesProvider.getAvailableZoneIds()),这是在开发区解析花费的时间99%的责任。
ZoneTextPrinterParser
parse()
ZoneRulesProvider.getAvailableZoneIds()
那么,答案可能是编写我自己的区域解析器,也不会太好,因为那样我就无法构建DateTimeFormattervia了appendPattern()。
DateTimeFormatter
appendPattern()
如您的问题和我的评论中所述,每次需要解析时区时,ZoneRulesProvider.getAvailableZoneIds()都会创建一组新的所有可用时区的字符串表示形式(的键static final ConcurrentMap<String, ZoneRulesProvider> ZONES)。1个
static final ConcurrentMap<String, ZoneRulesProvider> ZONES
幸运的是,a ZoneRulesProvider是一个abstract旨在被子类化的类。该方法protected abstract Set<String> provideZoneIds()负责填充ZONES。因此,如果子类提前知道要使用的 所有 时区,则只能提供所需的时区。由于该类提供的条目要少于包含数百个条目的默认提供程序,因此它有可能显着减少的调用时间getAvailableZoneIds()。
ZoneRulesProvider
abstract
protected abstract Set<String> provideZoneIds()
ZONES
getAvailableZoneIds()
该ZoneRulesProvider API提供了如何注册一个指令。请注意,不能取消注册提供程序,只能对其进行补充,因此,删除默认提供程序并添加您自己的提供程序不是一件简单的事情。系统属性java.time.zone.DefaultZoneRulesProvider定义默认提供程序。如果返回null(通过System.getProperty("..."),则将加载JVM的臭名昭著的提供程序。使用System.setProperty("...", "fully-qualified name of a concrete ZoneRulesProvider class")一个可以提供自己的提供程序,这就是第二段中讨论的提供程序。
java.time.zone.DefaultZoneRulesProvider
null
System.getProperty("..."
System.setProperty("...", "fully-qualified name of a concrete ZoneRulesProvider class")
最后,我建议:
abstract class ZoneRulesProvider
我自己没有这样做,但是我 确信它会因为某种原因而失败,因为 它会起作用。
1在问题注释中建议,在1.8版本之间,调用的确切性质可能已更改。
编辑: 找到更多信息
上述默认ZoneRulesProvider值final class TzdbZoneRulesProvider位于中java.time.zone。从路径中读取该类中的区域:(JAVA_HOME/lib/tzdb.dat在我的情况下,它在JDK的JRE中)。该文件确实包含许多区域,下面是一个片段:
final class TzdbZoneRulesProvider
java.time.zone
JAVA_HOME/lib/tzdb.dat
TZDB 2014cJ Africa/Abidjan Africa/Accra Africa/Addis_Ababa Africa/Algiers Africa/Asmara Africa/Asmera Africa/Bamako Africa/Bangui Africa/Banjul Africa/Bissau Africa/Blantyre Africa/Brazzaville Africa/Bujumbura Africa/Cairo Africa/Casablanca Africa/Ceuta Africa/Conakry Africa/Dakar Africa/Dar_es_Salaam Africa/Djibouti Africa/Douala Africa/El_Aaiun Africa/Freetown Africa/Gaborone Africa/Harare Africa/Johannesburg Africa/Juba Africa/Kampala Africa/Khartoum Africa/Kigali Africa/Kinshasa Africa/Lagos Africa/Libreville Africa/Lome Africa/Luanda Africa/Lubumbashi Africa/Lusaka Africa/Malabo Africa/Maputo Africa/Maseru Africa/Mbabane Africa/Mogadishu Africa/Monrovia Africa/Nairobi Africa/Ndjamena Africa/Niamey Africa/Nouakchott Africa/Ouagadougou Africa/Porto-Novo Africa/Sao_Tome Africa/Timbuktu Africa/Tripoli Africa/Tunis Africa/Windhoek America/Adak America/Anchorage America/Anguilla America/Antigua America/Araguaina America/Argentina/Buenos_Aires America/Argentina/Catamarca America/Argentina/ComodRivadavia America/Argentina/Cordoba America/Argentina/Jujuy America/Argentina/La_Rioja America/Argentina/Mendoza America/Argentina/Rio_Gallegos America/Argentina/Salta America/Argentina/San_Juan America/Argentina/San_Luis America/Argentina/Tucuman America/Argentina/Ushuaia America/Aruba America/Asuncion America/Atikokan America/Atka America/Bahia
然后,如果找到了一种仅用所需区域创建类似文件并加载该区域的方法,那么性能问题 可能 肯定无法解决。