JMM介绍
JMM是为了保证多个线程之间可以有效的、正确的地协同工作而出现的。JMM的关键技术点都是围绕多线程的原子性,可见性和有序性来建立的。
Java内存模型的抽象结构示意图如下:
从上图可以看出,如果线程A和线程B要通信的话,必须经过下面两个步骤。
1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。
JAVA内存模型是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。JMM为程序中所有的操作定义了一个偏序关系,称之为Happens-Before。
Happens-Before一些基本原则如下:
- 程序顺序原则:一个线程内保证语义的串行性
- 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
- 传递性:A先于B,B先于C,则A必然先于C
- volatile规则:volatile变量的写先于读发生,这保证了volatile变量的可见性
- 线程的start()方法先于它的每一个动作
- 线程的所有操作先于线程的终结(Thread.join())
- 线程的中断(interrupt)先于被中断线程的代码
- 对象的构造函数的执行、结束先于finalize()方法
下面我们来介绍一下volatile和volatile上面所说的Happens-Before规则。
volatile关键字
为了在合适的场合,确保线程之间的有序性、可见性、原子性,java使用了一些特殊的操作或者关键字来声明,volidate就是其中之一的关键字,另外还有synchronized,final等关键字。
- 特性
- 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
- 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性
volatile写-读建立的happens-before关系
volatile对内存可见性影响比它自身的特性更为重要。从JSR-133(即从JDK5开始),volatile变量的写-读可以实现线程之间的通信。
volatile的示例代码:
public class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer(){
a = 1; //1
flag = true; //2
}
public void reader(){
if(flag){ //3
int i = a; //4
}
}
}
假设线程A执行writer()方法之后,线程B执行reader()方法,这个Happens-Before关系可以分为3类:
1)程序次序规则::1 happens-before 2, 3 happens-before 4
2)volatile规则:2 happens-before 3
3)传递性规则: 1 happens-before 4
关系图如下:
我们可以看到A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。
volatile写-读的内存语义
volatile写内存语义:当写一个volidatle变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile读内存语义:当读一个volatile变量时,JMM会把线程对应的本地内存置为无效。线程接下来将从主内存读取中读取共享变量。
volatile内存语义的实现
实现volatile内存语义,JMM会分别限制编译器重排序和处理器重排序。
volatile重排序规则表如下图所示:
为了实现volatile的内存语义,编辑器会在生成字节码的时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
在JSR-133之前的旧的内存模型中,volatile的写-读没有锁的释放-获取所具有的内存语义。JSR-133增强了volitale的内存语义:严格限制编译器和处理器对volatile变量和普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。
volatile的使用
vaolatile变量很方便,但也存在一些局限性。volatile变量通常用做某个操作完成、发生中断或者状态的标志。也可以用于表示其它状态的信息,但使用需要非常小心,例如它无法保证一些复合操作(例如i++)的原子性(原子变量提供了“读-改-写”的原子操作,这种情况可以使用原子变量,能确保只有一个线程对变量执行写操作)。
当且仅当满足以下所有条件时,才应该使用volatile变量:
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
- 该变量不会与其他变量一起纳入不变性条件中
- 在访问变量时不需要加锁
总结
关键字volatile并不能代替锁,仅仅保证对单个volatile 变量的读/写具有原子性,锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。锁从功能上看更加强大。可以说volatile提供了一种比锁更轻量级的线程通信机制,在可伸缩性和执行性能上更具优势。
参考书籍:《Java高并发程序设计(第2版)》《Java并发编程实战》《Java并发编程的艺术》