java 内存模型JMM

时间:2022-03-28 20:52:26

1. 什么叫线程安全?

    当多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替运行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获取正确的结果,那这个对象是线程安全的。

                                                                                                                -- 《深入理解java虚拟机》

2. JMM介绍

 线程安全问题一般是因为线程的工作内存与主内存不一致和指令重排序导致。

在多线程环境下需要线程间能够完成线程间的通信,告知彼此的状态及执行结果,以保证线程安全。

JMM线程间的通信则是通过主内存与线程工作内存的同步实现的隐式通信。


2.1 共享变量 

    java中的所有实例域、静态域、数组元素都是存放在对内存中,该部分数据对于线程而言是共享的。

2.2 JMM 抽象结构

java 内存模型JMM

线程间隐式通信的过程 : 

      A线程执行前会从主内存中copy出共享变量的副本放入工作内存,线程执行完毕后需要将工作内存的结果写回到主内存,B线程执行时会从主内存获取最新的结果。该过程就完成了线程间的隐式通信。工作内存与主内存的同步过程由JMM本身去控制。

后面会介绍到一些锁的实现机制均基于此点。

    其中会有个隐含的问题,如果线程A的结果没有及时的写回到主存,则此时B线程读取到的共享变量就是“脏数据”.线程的并发问题由此产生。

   此处可以引申到java对工作内存和主内存控制的一些手段:

   volatile 关键字就是做这个事情的。

  volatile 关键字基于lock指令,该指令会做两件事 : 

  1. 将当前处理器的缓存行的数据写回到系统内存

  2.  这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效

      以此来保证多个线程间的共享变量的可见性。当然volatile关键字还有一些重排序的禁止策略,这个后续的博文中进行更新。



3. 重排序

一个好的内存模型实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标而进行奋斗:在不改变程序执行结果的前提下,尽可能提高并行度。JMM对底层尽量减少约束,使其能够发挥自身优势。因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种:

java 内存模型JMM

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

如图,1属于编译器重排序,而2和3统称为处理器重排序。这些重排序会导致线程安全的问题,一个很经典的例子就是DCL问题,这个在以后的文章中会具体去聊。针对编译器重排序,JMM的编译器重排序规则会禁止一些特定类型的编译器重排序针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序

那么什么情况下,不能进行重排序了?下面就来说说数据依赖性。有如下代码:

double pi = 3.14 //A

double r = 1.0 //B

double area = pi * r * r //C

这是一个计算圆面积的代码,由于A,B之间没有任何关系,对最终结果也不会存在关系,它们之间执行顺序可以重排序。因此可以执行顺序可以是A->B->C或者B->A->C执行最终结果都是3.14,即A和B之间没有数据依赖性。具体的定义为:如果两个操作访问同一个变量,且这两个操作有一个为写操作,此时这两个操作就存在数据依赖性这里就存在三种情况:1. 读后写;2.写后写;3. 写后读,者三种操作都是存在数据依赖性的,如果重排序会对最终执行结果会存在影响。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序

另外,还有一个比较有意思的就是as-if-serial语义。

as-if-serial

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提供并行度),(单线程)程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。比如上面计算圆面积的代码,在单线程中,会让人感觉代码是一行一行顺序执行上,实际上A,B两行不存在数据依赖性可能会进行重排序,即A,B不是顺序执行的。as-if-serial语义使程序员不必担心单线程中重排序的问题干扰他们,也无需担心内存可见性问题

文章摘自 : 【

https://github.com/CL0610/Java-concurrency/tree/master/3.java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%E4%BB%A5%E5%8F%8Ahappens-before%E8%A7%84%E5%88%99】