Java 内存模型和 JVM 内存结构真不是一回事 (2)

常说的 JVM 内存结构指的就是上文提交到运行时数据区,其中堆、方法区被线程共享,程序计数器、栈、运行时常量池被线程独享

它描述的是,在运行时,字节码和代码数据存储的位置。

内存模型

先抛开 Java 不说,先来看下内存模型是什么?维基百科中的定义:

In computing, a memory model describes the interactions of threads through memory and their shared use of the data.

意思就是,在计算中,内存模型描述了多线程如何正确的通过内存进行交互和使用共享数据。换句话说,内存模型约束了处理器对内存的读写。

cpu-memory.jpg

CPU 和内存之间通常会存在一层或多层高速缓存,这对单处理器可能没问题,但在多处理器系统中,可能就会出现缓存一致性问题,也就是当两个处理器(线程)同时读取相同内存位置会发生什么?什么情况下会看到相同的值?

缓存一致性问题,在并发编程中,又被称作可见性问题。内存模型处理器级别,为处理器彼此之间对内存写入结果的可见性,定义了充分必要条件:

强内存模型,一般说的是顺序一致性,所有内存操作存在一个全序关系,每个操作都是原子的且立即对所有处理器可见

弱内存模型,不限制处理器的内存操作顺序,而使用特殊指令刷新或者使本地缓存失效,以便看到其他处理器的写入,或使此处理器的写入对其他处理器可见,这些特殊指令被称为内存屏障

大多数处理器不会限制内存操作的顺序,多线程在执行时可能会出现让人困惑和违背直觉的结果。这是因为 CPU 为了充分利用不同类型存储器(寄存器、高速缓存、主存)的总线带宽,会将内存操作重新排序,以无序执行,这个动作称为内存排序指令重排序

重排序,也被称为编译器优化和处理器优化,因为它既可以发生在编译期间,也可以发生在 CPU 运行时。为了保证多线程的有序性,需要使用内存屏障禁止重排序

所以说,内存模型就是在硬件层面描述了使用内存屏障(刷新缓存或禁用指令重排序)解决多线程编程中的可见性有序性的问题。

Java 内存模型

Java 内存模型(下文简称 JMM)就是在底层处理器内存模型的基础上,定义自己的多线程语义。它明确指定了一组排序规则,来保证线程间的可见性。

这一组规则被称为 Happens-Before, JMM 规定,要想保证 B 操作能够看到 A 操作的结果(无论它们是否在同一个线程),那么 A 和 B 之间必须满足 Happens-Before 关系

单线程规则:一个线程中的每个动作都 happens-before 该线程中后续的每个动作

监视器锁定规则:监听器的解锁动作 happens-before 后续对这个监听器的锁定动作

volatile 变量规则:对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作

线程 start 规则:线程 start() 方法的执行 happens-before 一个启动线程内的任意动作

线程 join 规则:一个线程内的所有动作 happens-before 任意其他线程在该线程 join() 成功返回之前

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

怎么理解 happens-before 呢?如果按字面意思,比如第二个规则,线程(不管是不是同一个)的解锁动作发生在锁定之前?这明显不对。happens-before 也是为了保证可见性,比如那个解锁和加锁的动作,可以这样理解,线程1释放锁退出同步块,线程2加锁进入同步块,那么线程2就能看见线程1对共享对象修改的结果。

Java 提供了几种语言结构,包括 volatile, final 和 synchronized, 它们旨在帮助程序员向编译器描述程序的并发要求,其中:

volatile - 保证可见性有序性

synchronized - 保证可见性有序性; 通过管程(Monitor)保证一组动作的原子性

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zydfzp.html