瞎掰JVM:内存模型

0. 瞎掰

一切都从人类语言可以虚构和想象开始。抽象是为了什么,个人感觉是为了更好的传播和继承。我常去问别人你的容器是什么,你的模型是什么,其实就是想交流程序跑起来的基础环境是什么。如果你还没有基于非常底层的硬件开发,那么请接受一个上层程序环境构建的语境世界,恰好你是一个JAVA工作者,那我的建议就是先了解内存模型。

1. 理想内存模型

在这里插入图片描述

顺序一致性模型是一个被想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有
两大特性:

I 一个线程中的所有操作必须按照程序的顺序来执行。
II 所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

2.硬件级的内存模型

在这里插入图片描述

在并发场景下,该模型下有
一个优化:
CPU执行优化;

两个问题:
缓存一致性;
Out-Of-Order Execution;

三个特性
原子性,是指在一个操作中,CPU 不可以在中途暂停然后再调度,即不被中断操作,要不执行完成,要不就不执行。
可见性,是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性,即程序执行的顺序按照代码的先后顺序执行。

三个特性用来保证并发安全,可见性对应缓存一致性问题,有序性强调了Out-Of-Order Execution 的解决,原子性比较特殊,指令重排序和CPU执行指令优化可能引起非原子性。非原子性的另一个场景,如32位系统读写64位long/double 变量时,分为了两次读写事务,进程间进行总线裁决胜利方进行读写事务完成,未成功获得两次连续事务便会破坏原子性。缓存一致性协议由CPU缓存系统实现有 mesi 等,同样功能但效率低是总线锁技术,但总线锁的使用范围在读写事务中也有出现。这里存在伪共享的问题,影响并发性能性能。JVM或者disrupt 各有优化方案。
增加解释Out-Of-Order Execution,如指令级并行的重排序,现代处理器采用的指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。若数据不存在依赖性,处理器可以改变语句对应的机器指令的执行顺序

3. JMM 内存模型

在这里插入图片描述

JVM屏蔽了操作系统和硬件的实现,对于字节码的指令执行系统调用就是JVM,线程调度和内存管理由JVM管理, 内存系统就是JVM堆,非堆等内存结构,而JMM是上面单元的抽象关系表达。一个 Java 线程的创建本质上就对应了一个本地线程(native thread)的创建
JMM规定了线程的工作内存和主内存的交互关系,以及线程之间的可见性和程序的执行顺序.在发生数据竞争时,JMM 对正确同步的多线程程序的内存一致性做了如下保证,如果正确同步,程序(多个线程间)的执行将具有顺序一致性。即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。这里的同步是指广义上的同步,包括对常用同步原语(lock,volatile和final)的正确使用.但在细节上,两个模型具有不同:
I 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行
II 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序
III JMM不保证对64位的long型和double型变量的读/写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性.
留意的是,在JSR -133之前的旧内存模型中,一个64位long/ double型变量的读/写操作可以被拆分为两个32位的读/写操作来执行。从JSR -133内存模型开始(即从JDK5开始),仅仅只允许把一个64位long/ double型变量的写操作拆分为两个32位的写操作来执行,任意的读操作在JSR -133中都必须具有原子性(即任意读操作必须要在单个读事务中执行)。另外我强调以上规则适用普通64位变量。

同样该模型对三大特性的描述:

原子性
由Java内存模型来直接保证原子性的变量操作包括read、load、use、assign、store、write这6个动作,虽然存在long和double的特例,但基本可以忽律不计,目前虚拟机基本都对其实现了原子性。如果需要更大范围的控制,lock和unlock也可以满足需求。lock和unlock虽然没有被虚拟机直接开给用户使用,但是提供了字节码层次的指令monitorenter和monitorexit对应这两个操作,对应到java代码就是synchronized关键字,因此在synchronized块之间的代码都具有原子性。

可见性
可见性是指一个线程修改了一个变量的值后,其他线程立即可以感知到这个值的修改。正如前面所说,volatile类型的变量在修改后会立即同步给主内存,在使用的时候会从主内存重新读取,是依赖主内存为中介来保证多线程下变量对其他线程的可见性的。
除了volatile,synchronized和final也可以实现可见性。synchronized关键字是通过unlock之前必须把变量同步回主内存来实现的,final则是在初始化后就不会更改,所以只要在初始化过程中没有把this指针传递出去也能保证对其他线程的可见性。

有序性
有序性从不同的角度来看是不同的。单纯单线程来看都是有序的,但到了多线程就会跟我们预想的不一样。可以这么说:如果在本线程内部观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句说的就是“线程内表现为串行的语义”,后半句说的是“指令重排序”现象和主内存与工作内存之间同步存在延迟的现象。
保证有序性的关键字有volatile和synchronized,volatile禁止了指令重排序,而synchronized则由“一个变量在同一时刻只能被一个线程对其进行lock操作”来保证

补充几个概念:

数据依赖性是针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器和不同线程之间不做考虑。
I 不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。
II 所有的重排序都不会有数据依赖的操作做重排序,因为这样会改变最终的执行结果。

在这里插入图片描述
先行发生原则是Java内存模型中定义的两个操作之间的偏序关系。我的理解是JVM预定义了一些有序的JVM之上底层场景的默认规则以简化编程。这里偏序的描述又借助了可见性,可见性表示了线程之间交互的数据同步,有序性表示了线程之间交互的程序同步。

内存语义
先介绍volatile的内存语义
I 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
II 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

内存语义总结

I 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。
II 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
III 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

这与缓存一致性协议非常契合。包括volitile 内存的语义使用storestore(控制volitile写后的重排序)等内存屏障,其在硬件内存结果的对应也是内存屏障。
另一个需要注意的事CAS,CAS同时具有volatie读和写的内存语义。在CAS和volatile之上实现了丰富的锁和concurrent 基础。锁的释放和获取vlotile的写和读具有同样的内存语义,即通知其他即将读和写/获取和释放的程序。

最后反本溯源总结一下,为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。JMM便是基于共享内存实现的内存模型。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章