写了多年代码,你却不知道的程序设计的5个底层逻辑( 八 )


System.currentTimeMillis() 以及 System.nanoTime() 这两种方式都依赖于底层操作系统,前者是毫秒级,经测试 windows 平台的频率可能超过 10ms ,而后者是纳秒级别,频率在 100ns 左右,所以如果要获取更精准的时间建议用后者好了,api 了解完了,我们来看下定时器的底层是怎么实现的,现代PC机中有三种硬件时钟的实现,他们都是通过晶体振动产生的方波信号输入来完成时钟信号同步的 。

  • 实时时钟 RTC ,用于长时间存放系统时间的设备,即使关机也可以依靠主板中的电池继续计时 。Linux 启动的时候会从 RTC 中读取时间和日期作为初始值,之后在运行期间通过其他计时器去维护系统时间 。
  • 可编程间隔定时器 PIT ,该计数器会有一个初始值,每过一个时钟周期,该初始值会减1,当该初始值被减到0时,就通过导线向 CPU 发送一个时钟中断, CPU 就可以执行对应的中断程序,也就是回调对应的任务
  • 时间戳计数器 TSC , 所有的 Intel8086 CPU 中都包含一个时间戳计数器对应的寄存器,该寄存器的值会在每次 CPU 收到一个时钟周期的中断信号后就会加 1。他比 PIT 精度高,但是不能编程,只能读取 。
时钟周期:硬件计时器在多长时间内产生时钟脉冲,而时钟周期频率为1秒内产生时钟脉冲的个数 。目前通常为1193180 。
时钟滴答:当PIT中的初始值减到0的时候,就会产生一次时钟中断,这个初始值由编程的时候指定 。
写了多年代码,你却不知道的程序设计的5个底层逻辑

文章插图
【写了多年代码,你却不知道的程序设计的5个底层逻辑】 
Linux启动的时候,先通过 RTC 获取初始时间,之后内核通过 PIT 中的定时器的时钟滴答来维护日期,并且会定时将该日期写入 RTC,而应用程序的定时器主要是通过设置 PIT 的初始值设置的,当初始值减到0的时候,就表示要执行回调函数了,这里大家会不会有疑问,这样同一时刻只能有一个定时器程序了,而我们在应用程序中,以及多个应用程序之间,肯定有好多定时器任务,其实我们可以参考 ScheduledExecutorService 的实现 。
只需要将这些定时任务按照时间做一个排序,越靠前待执行的任务放在前面,第一个任务到了在设置第二个任务相对当前时间的值,毕竟 CPU 同一时刻也只能运行一个任务,关于时间的精度问题,我们无法在软件层面做的完全精准,毕竟 CPU 的调度不完全受用户程序控制,当然更大的依赖是硬件的时钟周期频率,目前 TSC 可以提高更高的精度 。
现在我们知道了,Java 中的超时时间,是通过可编程间隔定时器设置一个初始值然后等待中断信号实现的,精度上受硬件时钟周期的影响,一般为毫秒级别,毕竟1纳秒光速也只有3米,所以 JDK 中带纳秒参数的实现都是粗暴做法,预留着等待精度更高的定时器出现,而获取当前时间 System.currentTimeMillis() 效率会更高,但他是毫秒级精度,他读取的 Linux 内核维护的日期,而 System.nanoTime() 会优先使用 TSC ,性能稍微低一点,但他是纳秒级,Random 类为了防止冲突就用nanoTime生成种子 。
Java 如何和外部设备通信计算机的外部设备有鼠标、键盘、打印机、网卡等,通常我们将外部设备和和主存之间的信息传递称为 I/O 操作 , 按操作特性可以分为,输出型设备,输入型设备,存储设备 。现代设备都采用通道方式和主存进行交互,通道是一个专门用来处理IO任务的设备, CPU 在处理主程序时遇到I/O请求,启动指定通道上选址的设备,一旦启动成功,通道开始控制设备进行操作,而 CPU 可以继续执行其他任务,I/O 操作完成后,通道发出 I/O 操作结束的中断,处理器转而处理 IO 结束后的事件 。其他处理 IO 的方式,例如轮询、中断、DMA,在性能上都不见通道,这里就不介绍了 。当然 Java 程序和外部设备通信也是通过系统调用完成,这里也不在继续深入了 。




推荐阅读