JAVA并发之 - 1 - 硬件并发的挑战

377 阅读4分钟

开源项目推荐

Pepper Metrics是我与同事开发的一个开源工具(github.com/zrbcool/pep…),其通过收集jedis/mybatis/httpservlet/dubbo/motan的运行性能统计,并暴露成prometheus等主流时序数据库兼容数据,通过grafana展示趋势。其插件化的架构也非常方便使用者扩展并集成其他开源组件。
请大家给个star,同时欢迎大家成为开发者提交PR一起完善项目。

前言

要深入理解JAVA并发,只从JAVA封装好的各种API及线程模型去了解是不够的,JAVA程序运行在JVM实例上,对于操作系统来说,整个JVM+Java程序加在一起相当于一个用户态进程。而JAVA多线程,本质上来说,也可以理解为操作系统多线程,今天我们就来从操作系统及硬件层面聊聊多线程。

CPU是如何工作的

件体系非常的复杂,作者知识有限,这里仅根据个人理解进行总结归纳。

CPU内部利用晶振震荡产生时钟信号,时钟信号高低电平可以表述成0-1信号,按照固定的频率输出给PC程序计数器,CPU就可以根据PC寄存器里面的地址从指令寄存器当中一个一个的取出指令并执行,计算机就这样一直不停的循环下去。

CPU流水线技术与指令重排序

然而这种执行方式效率是很低的,因为每条指令必须执行完毕才能执行下一条指令,而相对于CPU寄存器,内存的速度是很慢的,更别说硬盘等其他外设了,所以CPU引入多级缓存机制和流水线机制提高指令执行吞吐量。
这里简单说一下流水线机制,CPU把一个机器指令分为多个指令阶段,这样利用多级流水线,多个CPU指令的各个指令阶段就会穿插执行,看上去多个指令变成并行执行了,那么也许看到这里你会问,并行执行?不会有问题吗,答案是不会,CPU会利用多种机制判断指令是否可以并行,会尽可能的让可以并行的指令,例如无依赖的指令,则并行执行。 而有依赖关系的指令,依然会顺序执行,这样就能既保证一定的并行又保证程序正确性。
按照前面的理解,我们可以很容易的想到,想提高程序性能,只要拼命提高计算机主频就好了,那么为什么现在还有多核的架构,这不是很复杂吗?没错,早期的计算机就是靠提升主频的方式提高性能的,也就是著名的摩尔定律,但是受到物理的限制,晶振在高速震荡的时候产生大量热量无法及时散热,而增加CPU面积又会增大晶振震动的距离增加延迟,当到达主频发展的瓶颈时,聪明的科学家就想出了多核的解决方案。

内存重排序与内存屏障

然而,前面说到的CPU流水线技术,虽然能够保证一个进程(线程)的程序执行顺序,但是如果程序是多线程的,多个线程运行在多个CPU上,而每个CPU都有自己的一套缓存,假设CPU1修改了变量a的数据并写入缓存还未刷新到主存的时候,CPU2也读取a的数据,这样就读到了旧的数据,这样从内存角度上看来,程序的执行实际上是乱序了,这也就是所谓的内存重排序。
说到这里,并发领域,个人认为最难理解,最晦涩的内存屏障就闪亮登场了,为了保证多线程并发程序的顺序正确性,CPU厂商各自制定了自己的处理器内存模型规则,如下表:


可见,面向硬件的编程是非常的复杂的,硬件厂商定义了各自的内存屏障规则,有的相对松散,越松散硬件能够乱序执行并优化性能的空间越大,而程序员想写出正确的并发程序也就越难,而JAVA语言的内存模型JMM则致力于定义一套统一的内存视图,使程序员能够容易理解,并屏蔽底层硬件的差异,让Java程序员能够更简单的写出正确同步的多线程并发程序,这个内存视图就是后面要讲到的happens-before规则。

总结

本篇,作者以个人浅薄的理解,简单梳理了一下,JAVA并发背后操作系统及硬件层面并发面临的挑战,以及操作系统,硬件厂商,乃至编译器开发者做出的努力及提供的工具,例如:内存屏障,缓存一致性协议(MESA),让我们能够对linux的多线程程序背后机制有一个大概的了解。

作者其他文章

github.com/zrbcool/blo…

微信订阅号