Java/Kotlin系统仿真时间戳的实现
仿真中的时间
系统仿真中,最少需要考虑两个时间。一个是仿真时间,一个是墙上时间。仿真时间是仿真系统中的时间推进过程,可能是均匀的,也可能是基于事件的非线性推进。墙上时间则是现实中的时间。仿真时间与墙上时间可能没有联系。在某些系统中,则必须维持二者之间的(比例)关系。
在分布式实时仿真或实物在回路的仿真系统中,需要维持仿真时间与墙上时间的同步关系。
仿真时间与墙上时间的关系是通过一个时钟同步算法来维持的。时钟同步算法的目的是维持仿真时间与墙上时间的关系,使得仿真时间与墙上时间的差值保持在一个可接受的范围内。
Java/Kotlin中的时间戳
实现任何时间同步算法,都离不开本地系统的时间戳。
在JVM中,时间戳的精度是毫秒级别的。
在Java/Kotlin中,一般通过System.currentTimeMillis()
来获取当前时间戳,比如在计算算法的执行时间时,可以这样做:
val start = System.currentTimeMillis()
// do something
Thread.sleep(100)
val end = System.currentTimeMillis()
println("cost: ${end - start} ms")
cost: 113 ms
fun prod(n : Long): Long {
var p = 1L
for (i in 1..n) {
p *= i
}
return p
}
fun timeIt(timeFunc: ()->Long, n: Int, func: ()->Unit): Double {
var t = 0.0
for (i in 1..n) {
val t0 = timeFunc()
func()
val t1 = timeFunc()
t += (t1 - t0).toDouble()
}
return t/n
}
fun timeHist(timeFunc: ()->Long, n: Int, func: ()->Unit): List<Double> {
val t = mutableListOf<Double>()
for (i in 1..n) {
val t0 = timeFunc()
func()
val t1 = timeFunc()
t.add((t1 - t0).toDouble())
}
return t
}
首先,进行prod(1000000)
运行时间的比较,采用两种不同的计时方式。值得注意的是,可以用System::currentTimeMillis
来引用方法,也可以构造lambda表达式{System.nanoTime()}
来引用方法。
// measure time in milliseconds
val dtMillis = timeIt(System::currentTimeMillis, 10000) {
prod(1000000)
}
// measure time in nanoseconds
val dtNano = timeIt(System::nanoTime, 10000) {
prod(1000000)
}
println("${dtMillis} ms")
println("${dtNano} ns")
1.4356 ms
1435843.23 ns
基本上,JVM中所有的时间戳的精度都是相仿的,均试图通过调用最高精度的系统时间戳来实现。但是currentTimeMillis
在某些时候精度是不足以表达,因此还需要利用nanoTime
来获取更高精度的时间戳。
下面的比较可以更好的表明这一点。
%use lets-plot
获得数据,并都转换为毫秒:
// collect data for plotting
val data = mapOf(
"ms" to timeHist(System::currentTimeMillis, 10000) {
prod(1000000)
},
"ns" to timeHist({System.nanoTime()}, 10000) {
prod(1000000)
}.map { it/1000000.0 }
)
可以把上面的数据绘制成如下的图表:
// lets-plot: results
var p = letsPlot(data)
p += geomHistogram(binWidth = 0.2, color = "blue", fill="blue"){x="ms"}
p += geomHistogram(binWidth = 0.2, color = "red", fill="red"){x="ns"}
p += ggsize(600, 400)
p + ggtitle("Time in milliseconds") + xlab("Measured Time") + ylab("Count")
从上图可以看到,currentTimeMillis
得到大概一定比例的不同值,最终平均得到结果。而nanoTime
可以直接到结果。
Instant的使用
在实际开发中,并仅仅需要知道两个时间点的差,还要把时间戳转换成时间,这时候就需要用到Instant
了。Instant
是Java8中引入的一个时间戳类,它的分辨率是纳秒级别的。它所返回的时间戳是从1970年1月1日0时0分0秒开始的纳秒数。这个时间起点也称为UTC时间起点。
在此基础上,还需要定义时区的概念。时区是指相对于UTC时间起点的偏移量。比如北京时间是UTC时间起点的8小时偏移量,那么北京时间的起点就是1970年1月1日8时0分0秒。
import java.time.Instant
import java.time.ZoneId
在Java中,时区的定义是通过ZoneId
来实现的。ZoneId
是一个时区的标识符,比如Asia/Shanghai
就是北京时间的时区标识符。通过下面的代码可以时区标识的列表:
val asiaZoneIds = ZoneId.getAvailableZoneIds().filter { it.startsWith("Asia/C") }.sorted()
println("Aisa Cities starting with 'C': $asiaZoneIds")
Aisa Cities starting with 'C': [Asia/Calcutta, Asia/Chita, Asia/Choibalsan, Asia/Chongqing, Asia/Chungking, Asia/Colombo]
时间戳和当地时间的转换代码如下:
val zone = ZoneId.of("Asia/Chongqing")
val now = Instant.now()
println("UTC时间零点 \t: ${Instant.EPOCH}")
println("UTC时间戳 \t: ${now.epochSecond} ms + ${now.nano} ns")
println("当前时间 \t: ${now.atZone(zone)}")
val sec = now.epochSecond
val nano = now.nano.toLong()
fun localTime(sec: Long, nano: Long, zone: ZoneId) = Instant.ofEpochSecond(sec, nano).atZone(zone)
println("时间戳->时间 \t: ${localTime(sec, nano, zone)}")
UTC时间零点 : 1970-01-01T00:00:00Z
UTC时间戳 : 1682221288 ms + 773663800 ns
当前时间 : 2023-04-23T11:41:28.773663800+08:00[Asia/Chongqing]
时间戳->时间 : 2023-04-23T11:41:28.773663800+08:00[Asia/Chongqing]
有上面的例子,在仿真程序中就可以这样做:
fun timeStamp() : Long {
val now = Instant.now()
return now.toEpochMilli() * 1000000 + now.nano
}
fun timeFromStamp(stamp: Long) : Instant {
val ms = stamp / 1000000
val ns = stamp % 1000000
return Instant.ofEpochMilli(ms).plusNanos(ns)
}
val stamp = timeStamp()
println("时间->时间戳 \t: ${stamp}")
println("时间戳->时间 \t: ${timeFromStamp(stamp).atZone(zone)}")
时间->时间戳 : 1682221291064901100
时间戳->时间 : 2023-04-23T11:41:31.064901100+08:00[Asia/Chongqing]
总结
- JVM有一个高精度的时钟,能给出纳秒为单位的结果,但是实际精度不好说;
- 本地时间和时间戳采用UTC时间零点可以进行转换。
- 在仿真中,可以用纳秒单位的Long来表达。