一、前言
在java多线程编程中,volatile可以用来定义轻量级的共享变量,它比synchronized的使用成本更低,因为它不会引起线程上下文的切换和调度。所谓知己知彼、百战不殆。本文从JVM内存模型角度,探讨volatile的实现原理。在明白了volatile的实现原理后,再回过头来使用它,会有一种一览众山小的感觉吧,使用起来会更加得心应手。
二、Java内存模型(JMM)
1、并发编程关键问题
多线程编程涉及到两个关键问题,线程之间的通信与同步。通信是指线程之间传递信息,同步是指控制线程操作的执行顺序。通过共享内存或者消息通知这两种方法,可以实现通信或同步。基于共享内存的线程通信是隐式的,线程同步是显式的;而基于消息通知的线程通信是显式的,线程同步是隐式的。JAVA是前者,即基于共享内存的隐式线程通信、显式线程同步。
2、happens-before模型
JMM呈现给程序员的模型是happens-before模型,即:
- 顺序规则:(单)线程中的写操作的结果,happens before于于任意后续操作。
- 锁规则: 锁的解锁,happens before于于锁的获取或加锁。
- volatile变量规则:volatile写操作,happens before于后续该变量的读操作是可见的。
- 传递性:A操作happens before于B操作,B操作happens before于C操作,则A操作happens before于C操作
这儿的happens-before,并不是指操作先于后续操作执行,而是指操作结果对于后续结果是可见的。
3、可见性
在JMM中,每个线程的内存由两层构成:线程的“本地内存”、“主内存”。“本地内存”是JMM的一个抽象,本身是不存在的,它包括缓存、寄存器、写缓冲区、编译器及CPU的优化等。共享变量存放在“主内存”中,“本地内存”中存放的是共享变量的副本。当线程中发生对共享变量的写操作时,并不是直接写到“主内存”中的,而是先写到“本地内存”的写缓冲区中,只有当刷新(flush)到主内存后,才可能被其它线程加载到其“本地内存”中,此时我们说该共享变量是对其它线程“可见”的,反之如果没有刷新(flush)到主内存,就是对于其它线程“不可见”的。
JMM就是通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供可见性的保证的
三、volatile类型的内存语义
happens-before规则提供了对volatile变量可见性的保证,即volatile变量的写操作,对后续任意对该变量的读操作是可见的。
四、volatile类型的JMM实现
再深入一些,JMM是如何实现happens-before模型中的volatile规则的呢?是通过限制这两种操作的重排序实现的。
重排序规则 | 第二个操作 | ||
---|---|---|---|
第一个操作 | volatile读 | volatile写 | 普通读/写 |
volatile读 | 禁止重排序(1) | 禁止重排序(2) | 禁止重排序(3) |
volatile写 | 禁止重排序(4) | 禁止重排序(5) | – |
普通读/写 | – | 禁止重排序(6) | – |
(1) 第一个操作是volatile读时,不可以重排序,否则读出来的结果,可能是被修改过了的。
(2)(5)(6),第二个是volatile写时,所有操作都不可以被重排序于其后面,因为要确保其写的结果对于后续操作可见。
五、volatile类型使用的注意事项
- 多个volatile操作或复合操作,整体上不具有有序性
- 适用于对volatile变量的写操作很少而读操作很多的环境