寻求不受系统/BIOS时钟修改影响的单调递增计时方案
跨重启且不受时钟修改影响的计时实现方案
你之前遇到的问题本质是:
System.nanoTime()是JVM级别的单调时钟,跨JVM重启后无法关联之前的状态,无法构建全局时间线RuntimeMXBean.getUptime()仅统计应用运行时长,完全忽略系统关机期间的时间
以下是满足你需求的可行方案:
核心思路
结合不受系统时钟调整影响的单调时钟(计算应用运行时段的流逝时长)和UTC实时时间戳(计算系统关机时段的流逝时长),通过本地持久化存储(如JSON文件、SQLite)记录关键时间点,跨应用/系统重启后合并两段时间,得到真实的总流逝时长。
方案1:Java 9+ 原生支持(推荐)
Java 9及以上提供了Clock.systemMonotonic(),这是基于系统级单调时钟的实现,完全不受系统时钟修改(包括BIOS调整)的影响,精度足够满足需求。
具体步骤
初始化保留会话
将以下数据持久化到本地存储:- 总保留时长:
totalRetentionMs(例如60 * 60 * 1000) - 当前单调时钟值:
monotonicStart = Clock.systemMonotonic().millis() - 当前UTC时间戳:
utcStart = Instant.now().toEpochMilli() - 剩余保留时长:
remainingMs = totalRetentionMs
- 总保留时长:
应用运行时计时
- 每隔固定间隔(如1秒),计算当前单调时钟与
monotonicStart的差值runElapsedMs - 更新
remainingMs = totalRetentionMs - runElapsedMs - 若
remainingMs <= 0,直接标记会话过期
- 每隔固定间隔(如1秒),计算当前单调时钟与
应用关闭时更新状态
重新持久化以下数据:monotonicStart改为当前单调时钟值utcStart改为当前UTC时间戳remainingMs改为当前剩余时长
应用重启恢复状态
- 读取持久化的
remainingMs、monotonicStart、utcStart - 获取当前值:
currentMonotonic = Clock.systemMonotonic().millis(),currentUtc = Instant.now().toEpochMilli() - 计算两段流逝时间:
- 上次应用运行时长:
runElapsed = currentMonotonic - monotonicStart - 系统关机时长:
shutdownElapsed = currentUtc - utcStart
- 上次应用运行时长:
- 处理时钟回拨:如果
shutdownElapsed < 0(用户将系统时间调回过去),则shutdownElapsed = 0 - 更新剩余时长:
remainingMs = remainingMs - (runElapsed + shutdownElapsed) - 若
remainingMs <= 0,标记会话过期;否则更新持久化的monotonicStart和utcStart为当前值,继续计时
- 读取持久化的
方案2:Java 8及以下兼容方案
如果应用基于Java 8或更低版本,需通过JNA/JNI调用系统原生API获取单调时钟:
- Windows:调用
QueryPerformanceCounter获取计数,QueryPerformanceFrequency获取频率,换算为毫秒数 - Linux/macOS:调用
clock_gettime(CLOCK_MONOTONIC_RAW)获取单调时间戳
后续的持久化、计时逻辑与方案1完全一致,仅替换单调时钟的获取方式。
方案3:降级方案(无单调时钟支持时)
如果系统不支持单调时钟(极少情况),可仅用UTC时间戳,但增加时钟回拨检测:
- 每次记录UTC时间戳时,同时保存上次的时间戳
- 若当前UTC时间早于上次记录的时间,说明时钟被回拨,此时忽略本次时间差,仅用
System.nanoTime()的差值计算应用运行时长 - 正常情况下,用当前UTC与上次记录的UTC差值计算总流逝时间
该方案精度略低,无法完全避免时钟修改带来的误差,但能满足基本需求。
关键注意事项
- 持久化存储需保证原子性:写入状态时可使用临时文件写入后替换原文件,或用数据库事务,避免应用崩溃导致状态损坏
- 极端情况处理:如果用户将系统时间调快超过剩余保留时长,直接判定会话过期
- 单位统一:单调时钟和UTC时间戳的单位需统一(如均用毫秒),避免换算错误
内容的提问来源于stack exchange,提问作者DistantBlue
相关产品推荐
相关产品推荐

