JMM
JMM对共享内存的操作做出了如下两条规定:
- 线程对共享内存的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;
- 不同线程无法直接访问其他线程工作内存中的变量,因此共享变量的值传递需要通过主内存完成。
可见性问题
在多线程应用程序中,线程对非易失性变量(non-volatile variables)进行操作,出于性能原因,每个线程在处理变量时会将变量从主内存复制到CPU缓存中。如果您的计算机包含多个CPU,每个线程可能运行在不同的CPU上。这意味着,每个线程可以将变量复制到不同CPU的CPU缓存中。
假设两个或多个线程访问一个共享对象,该对象包含如下声明的计数器变量:
public class SharedObject {
public int counter = 0;
}
假设只有线程1增加了计数器变量,但线程1和线程2可能会不时地读取计数器变量。如果计数器变量不是声明为volatile,则无法保证计数器变量的值何时从CPU缓存写到主存。这意味着,CPU缓存中的计数器变量值可能与主存中的计数器变量值不同。
线程看不到变量的最新值,因为它还没有被另一个线程写回主内存,这个问题被称为“可见性”问题。一个线程的更新对其他线程是不可见的。
可见性解决方案
Happens-Before
从JDK5开始,Java使用新的JSR -133内存模型。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在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关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。
happens-before与JMM的关系如下图所示:
如上图所示,一个happens-before规则通常对应于多个编译器和处理器重排序规则。对于java程序员来说,happens-before规则简单易懂,它避免java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现。
volatile
Java volatile关键字旨在解决变量可见性问题。通过声明计数器变量volatile,所有对计数器变量的写入都将立即写回主存。同样,对计数器变量的所有读取都将直接从主存中读取。
public class SharedObject {
public volatile int counter = 0;
}
在上面给出的场景中,一个线程(T1)修改计数器,而另一个线程(T2)读取计数器(但从不修改它),声明计数器变量volatile足以保证对T2写入计数器变量的可见性。因此,声明变量volatile可以保证其他线程对该变量的写入是可见的。
使用volatile修饰一个共享变量可以达到如下的效果:
- 一旦线程对这个共享变量的副本做了修改,会立马刷新最新值到主内存中去;
- 一旦线程对这个共享变量的副本做了修改,其他线程中对这个共享变量拷贝的副本值会失效,其他线程如果需要对这个共享变量进行读写,必须重新从主内存中加载。
volatile可见性规范:
- 对volatile变量执行写操作时,会在写操作后加入一条store写屏障指令,强制将缓存刷新到主内存中
- 对volatile变量执行读操作时,会在读操作前加入一条load读屏障指令,强制使缓冲区缓存失效,所以会从主内存读取最新值。
- 防止指令重排序。
共享变量在线程间不可见的原因 | volatile解决方案 |
---|---|
重排序 & 线程交叉执行 | 防止指令重排序 |
共享变量未及时更新 | 通过volatile可见性规范 |
synchronized
使用synchronized代码块或者synchronized方法也可以保证共享变量的可见性。
JMM关于synchronized的规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存中
- 线程加锁时,将清空工作内存中存储的共享变量的值,从而使用共享变量时,必须从主内存中重新读取最新的值。(注意:解锁和加锁,是指同一把锁)
因此线程执行synchronized代码执行互斥锁的过程:
- 获得互斥锁。
- 清空工作内存。
- 从主内存拷贝变量的最新副本到工作内存。
- 执行代码
- 将更改后的共享变量的值刷新到主内存中
- 释放互斥锁
synchronized还会限制编译器、运行时和硬件对内存操作重排序的方式,从而在实施重排序时不会破坏JMM提供的可见性保证。
共享变量在线程间不可见的原因 | synchronized解决方案 |
---|---|
重排序 & 线程交叉执行 | 原子性(结合as-if-serial语义) |
共享变量未及时更新 | 通过synchronized可见性规范 |