程序的高效并发(从硬件效率一致性->内存模型协议->JVM)

程序的高效并发(从硬件效率一致性->内存模型协议->JVM)

加油站:从外打破是食物,从内打破是生命;

前言:

计算机性能发展源动力的根本原因在于两条定律:
定律一:摩尔定律:
用于描述处理器晶体管数量与运行效率之间的发展关系;
定律二:Zmdahl定律:
通过系统中并行化与串行化之间的比重来描述多处理器系统能获得的运算加速能力;
这两条定律的更替代替了近几年来硬件发展从追求的处理器频率到追求多核心并行处理的发展历程;

正文:

硬件的效率与一致性:

物理机遇到的并发问题和java虚拟机中的情况还是有很多相似之处的,所以物理机对并发的处理方案对于其他语言虚拟机的实现有很大的参考价值.

提出场景进行分析:
绝大多数的运算任务都不可能只靠处理器计算就可以完成的,处理器至少要与内存进行交互,举例子,如:运算数据,存储运算结果,I/O操作,无法只依靠寄存器来完成,但由于计算机的存储设备与处理器的运算速度有几个数量级的差距,大量的时间都花费在磁盘I/O,网络通信上.

解决方案:
针对于这中速度不一致的问题,现代计算机系统都加入了一层读写速度尽可能接近处理器运算速度的高速缓存,以此来作为内存与处理器之间的缓冲,让运算可以快速进行,当运算结束后再从缓存同步到内存之中,处理器就不需要等待缓慢地内存读写了;

引入新问题: 缓存一致性
高速缓存的存储交互很好的解决了处理器与内存之间的速度矛盾,但是也为系统带来了更高的复杂性,在多处理器系统中,每个处理器有自己的高速缓存,但又共享同一主内存,当各个处理器的运算任务涉及到同一块主内存区域时,将会导致各个的缓存内存不一样,即:同步到主内存中以谁的缓存为主呢?

解决缓存一致性问题:
为了解决缓存一致性问题,让每个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,这些协议有:MAI,MOSI,Protocol等,继而,我们常说的"内存模型"一词也就问世了,官方解释下内存模型:在特定的操作写一下,对特定的内存和告诉缓存进行读写访问的过度抽象. 不同架构的物理机可以拥有不一样的内存模型,而java虚拟机也有自己的内存模型,以上介绍的是硬件的缓存访问操作;

一张图片说明此时的解决方案:
在这里插入图片描述
处理器输入代码乱序执行(Out-Of-Order Execution):
除了增加告诉缓存之外,为了使处理器内部单元充分被利用,处理器有可能对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果进行重组,保证该结果与顺序执行的结果是一致的,但是并不保证各个语句计算的先后顺序与输入代码中的顺序一致.在java虚拟机中,即时编译器也有类似的指令重排序优化;

java内存模型-(Java Memory Model,JMM):

定义java内存模型需考虑事项:
通过对硬件和操作系统的的访问差异,以及主流语言(C/C++等)直接使用物理硬件和操作系统的内存模型这种不同平台上内存模型的差异,定义java内存模型并非一种容易的事情,这个模型必须足够严谨,足够宽松,这两者并没有矛盾之处:足够严谨是保证java内存访问不会出现歧义,足够宽松使得虚拟机的实现有足够的自由空间去利用硬件的各种特性,比如:寄存器,高速缓存,指令等,以此来获得会更好的执行速度.
在长时间的验证和修补,JDK1.5发布后,java内存模型已经成熟和完善了.

java主内存和工作内存:
java内存模型的主要目标是定义程序中的各个变量的访问规则,即在虚拟机中将变量存储到内存以及从哪个内存中取出变量这样的底层细节(公有方面),这种变量是指实例字段,静态字段和构成数组对象的元素,而不是私有线程具有的局部变量和方法参数.

java内存模型规定了所有的变量都存在出在主内存中(Main Memory),每条线程有自己的工作内存,线程对变量的所有的操作(读取,赋值等)都必须在工作内存中进行,即私有线程,而不能直接读写主内存中的变量,不同线程之间也无法直接访问对方工作内存中的变量,线程之间变量之间传递均通过主内存来完成;

下图是对于上面描述线程,主内存以及工作内存三者的交互关系图说明:在这里插入图片描述
注:主内存,工作内存与java内存区域中的java堆,java栈,方法区不是同一层次的内存划分,二者基本没有关系,若对应起来,即:主内存->java堆中的对象实例,工作内存->虚拟机栈中的部分区域.从更低的层次来说:主内存->物理硬件的内存;

内存间的交互操作:
  关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
1> lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
2> unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
3> read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
4> load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
5> use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
6> assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
7> store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
8> write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

执行八种操作需满足规则:

先举个例子:
如果把一个变量从主内存复制到工作内存,就要顺序的执行read和load操作,即读主内存->载入工作内存,要求两个操作必须顺序执行,而没有保证连续执行.
类似上面描述,执行八种基本操作需要满足如下规则:
1> 不允许read和load、store和write操作之一单独出现
2> 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
3> 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
4> 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
5> 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
6> 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
**7>**如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
8> 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
以上八种内存访问操作及上面规则限定,再加上后文介绍的volitile的一些特殊规定,可以完全确定java程序中哪些此内存访问操作在并发下是安全的;

总结:

本文详细的介绍了从近年来硬件发展从追求处理器频率到追求多核心并行处理的抽象发展过程,以及对后来开发java虚拟机带来很高的可比性,然后详细介绍了java内存模型的相关内容.
在后续文章中,会详解volatile型变量的特殊规则(可见性,禁止指令重排序)详解,以及由此引出来的原子性,可见性,有序性,先行发生原则,和大家一起学习.
在阅读书籍《程序是怎样跑起来的》,《java高级特性》,《java虚拟机》等大量权威书籍所写个人见解文章,保证质量,保证原创,精彩持续进行中,感谢您的关注-微信公众号:十点攀程

下面是小编整理免费资源:
没别的,就是免费送资源(小编私藏,商业勿扰,仅限一天,java,python,数据结构…)
微信公众号: 十点攀程

发布了2 篇原创文章 · 获赞 0 · 访问量 1869

猜你喜欢

转载自blog.csdn.net/qq_41904194/article/details/104115614