Java第二刀 内存模型

Java第二刀 内存模型

关键字

  • 缓存一致性
  • 原子性
  • 有序性

正文

在理解内存模型前,先了解一下程序运行的过程,通过程序运行过程来说明缓存的重要性。

  • 运行hello world程序
    • 源码如下:

    • 程序运行过程:

    程序经过编译,在命令行输入指令./hello后,hello目标文件中的代码和数据会从磁盘复制到主内存。数据包括最终会被输出的字符串“hello,word\n”。一旦目标文件hello中的代码和数据被家在到主内存,处理器就开始执行hello程序的Main程序中的机器语言指令。

    这些指令将“hello,world\n”字符串中的字节从主内存复制到寄存器文件,寄存器再将数据复制到显示设备,最终显示到屏幕上。如下图:

    • 高速缓存的重要性:

处理器从寄存器中读数据的速度比从主内存中读取数据几乎要快100倍。随着半导体技术的进步,处理器与主内存之间的差距还在持续增大。加快处理器的运行速度比加快主内存的运行速度要容易。面对这种差异,系统设计者采用了更小、更快的存储设备,即高速缓存存储器,用来存放处理器近期可能会需要的信息。比较新的处理器,有三级高速缓存L1、L2、L3,利用高速缓存可以加快访问速度,将程序性能提高一个数量级。

  • 多级缓存与一致性问题

    当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。在有了多级缓存之后,程序的执行就变成了:
    当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致,如下图所示

  • 有序性

    处理器与编译器为了对操作进行优化,可能会对操作指令做重排序。编译器和处理器在重排序时,不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果,如下代码所示:

    从代码可以看出操作A与操作C有依赖关系,操作B与操作C有依赖关系,那么操作A与操作B,是可以被编译器与处理重排序的。在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果。

    但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果,如下代码所示:

    针对上面的代码,线程T1、T2同时执行。线程T2执行write方法,假设线程T2已经执行完B这个步骤,同时线程T1执行read方法,假设线程T1已经执行完C这个步骤,那么问题来了,此时的i的最终值是4还是2呢?答案是,i的最终结果是不可预知的,因为步骤A、B可能被处理器或编译器重排序。

  • 原子性

    多线程场景中操作如果不能保证原子性,会导致处理结果和预期不一致。比如经典的 i++ 操作,对于一个简单的i++操作,一共有三个步骤:load , add ,save 。共享变量就会被多个线程同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,如下代码所示:

    针对上面的代码,线程T3、T4同时执行,并且T3、T4同时执行到步骤A,线程T3、T4看到i的值都是0,此时cpu将执行时间分配给T3,T4进入就绪状态放弃cpu,当T3执行完步骤C时(i=1),此时Cpu重新调度,T3进入就绪状态放弃cpu,T4进入执行状态(i=0),T4执行步骤C(i=1),执行完毕后,因为这三个操作不是原子的,结果与预期不一致(i=2).

  • 内存模型

    线程之间的通信机制有两种:共享内存和消息传递(actor模型)。
    Java 的并发采用的是共享内存模型,Java 内存模型的抽象示意图如下:

    由上图可以看出,其实是各个线程的本地内存之间的数据互相不可见。
    JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

  • happens-before

    JSR-133 提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。 与程序员密切相关的 happens-before 规则如下:

    程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。

    监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。

    volatile 变量规则:对一个 volatile 域的写,happens- before 于任意后续对这个 volatile 域的读。

    传递性:如果 A happens- before B,且 B happens- before C,那么 A happens- before C。

    happens-before 与 JMM 的关系如下图所示:

总结

内存模型提供的内存可见性保证,降低了程序员去学习复杂的重排序规则以及这些规则的具体实现,是并发编程的理论基础。

本篇内容从下面的资料整理而来:)

参考资料

  • Hollis的星球
  • 深入理解计算机系统 第二版
  • https://www.infoq.cn/article/java-memory-model-1/
JackLei
JackLei

我是真的不会修电脑