Java并发编程3--认识Volatile和JMM

文章目录

    • 1.初步认识 Volatile
      • 一段代码引发的思考
      • volatile 的作用
      • volatile 关键字是如何保证可见性的?
    • 2.JMM
      • 什么是 JMM
      • 重排序
      • JMM 层面的内存屏障
      • HappenBefore

本文很多借鉴( Java并发编程的艺术 方腾飞 魏鹏 程晓明 著),读好书,读正版书。

1.初步认识 Volatile

一段代码引发的思考

public class VolatileDemo {
	public static boolean stop = false;

	public static void main(String[] args) throws InterruptedException {
		Thread thread = new Thread(() -> {
			int i = 0;
			while (!stop) {
				i++;
			}
		});
		thread.start();
		System.out.println("begin start thread");
		Thread.sleep(1000);
		stop = true;
	}
}	

上面这段代码,演示了一个使用 volatile 以及没使用volatile 这个关键字,对于变量更新的影响。不使用volatile该循环不会结束,使用了volatile 则会跳出循环。

volatile 的作用

volatile 可以使得在多处理器环境下保证了共享变量的可见性(线程通信的两种机制:共享内存、消息传递),在多线程环境下,读和写发生在
不同的线程中的时候,可能会出现:读线程不能及时的读取到其他线程写入的最新的值。这就是所谓的可见性。为了实现跨线程写入的内存可见性,必须使用到一些机制来实现。而 volatile 就是这样一种机制。如果一个字段被声明成volatile,Java线程内存 模型确保所有线程看到这个变量的值是一致的。

volatile 关键字是如何保证可见性的?

在修改带有 volatile 修饰的成员变量时,会多一个 lock 指令。lock是一种控制指令,在多处理器环境下,lock 汇编指令可以基于总线锁或者缓存锁的机制来达到可见性的一个效果。主要做了两件事:

  1. 将当前处理器缓存行的数据写回到系统内存。
  2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

2.JMM

什么是 JMM

JMM 全称是 Java Memory Model,java内存模型. 什么是 JMM 呢?导致可见性问题的根本原因是缓存以及重排序。 而 JMM 实际上就是屏蔽了各种硬件和操作系统的访问差异的,提供了合理的禁用缓存以及禁止重排序的方法。所以它最核心的价值在于解决可见性和有序性。

它定义了共享内存中多线程序读写操作的行为规范: 在虚拟机中把共享变量存储到内存以及从内存中取出共享变量的底层实现细节。 通过这些规则来规范对内存的读写操作从而保证指令的正确性,它解决了 CPU 多级缓存、处理器优化、指令重排序导致的内存访问问题,保证了并发场景下的可见性。

看下图(摘自 Java并发编程的艺术 方腾飞 魏鹏 程晓明 著),JMM 抽象模型分为主内存、工作内存(本地内存);主内存是所有线程共享的,一般是实例对象、静态字段、数组对象等存储在堆内存中的变量。工作内存是每个线程独占的,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量,线程之间的共享变量值的传递都是基于主内存来完成。

Java并发编程3--认识Volatile和JMM_第1张图片
从图中来看,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。

  1. 线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 线程B到主内存中去读取线程A之前已更新过的共享变量。

下面这张图解释了上面的两个步骤(摘自 Java并发编程的艺术 方腾飞 魏鹏 程晓明 著):
Java并发编程3--认识Volatile和JMM_第2张图片
本地内存A和本地内存B由主内存中共享变量x的副本。假设初始时,这3个 内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存 A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内 存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时 线程B的本地内存的x值也变为了1。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要 经过主内存。 JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供 内存可见性保证。

重排序

为什么代码会重排序?
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:

  • 在单线程环境下不能改变程序运行的结果。
  • 存在数据依赖关系的不允许重排序

重排序分3种类型

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

从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序:
在这里插入图片描述
上述的1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序 出现内存可见性问题。

  • 对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排 序(不是所有的编译器重排序都要禁止)。
  • 对于处理器重排序,JMM的处理器重排序规则会要 求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为 Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。

JMM 层面的内存屏障

为了保证内存可见性,Java 编译器在生成指令序列的适当位置会插入内存屏障来禁止特定类型的处理器的重排序,在 JMM 中把内存屏障分为四类:
Java并发编程3--认识Volatile和JMM_第3张图片
当基于共享可变状态的内存操作被重新排序时,程序可能行为不定。一个线程写入的数据可能被其他线程可见,原因是数据写入的顺序不一致。适当的放置内存屏障,通过强制处理器顺序执行待定的内存操作来避免这个问题。

HappenBefore

它的意思表示的是前一个操作的结果对于后续操作是可见的,所以它是一种表达多个线程之间对于内存的可见性。所以我们可以认为在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这个操作必须要存在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的关系:

Java并发编程3--认识Volatile和JMM_第4张图片
注:两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。很早之前写的介绍happens-before

你可能感兴趣的:(java并发编程)