JMM定义了一套在多线程读写共享数据时(成员变量,数组),对数据的可见性,原子性,有序性的规则和保障
1.保证原子性
原子性:要么全部执行,要么全不执行。
Java中有两种方式实现原子性
一种是使用锁机制,锁具有排他性,也就是说它能够保证一个共享变量在任意一个时刻仅仅被一个线程访问,这就消除了竞争;另一种CAS指令。
怎么保证原子性?
加锁:synhronized、Lock
在java中提供了两个高级的字节码指令monitorenter和monitorexit,使用对应的关键字Synchronized来保证代码块内的操作是原子的
2.保证可见性
举例:退不出的循环
public class Test {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (run){
//......
}
},"T1").start();
Thread.sleep(1000);
run = false;
}
}
- 为什么会退不出循环呢?
线程频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存的高速缓存中,减少对主存中的run的访问,提高效率
在1秒之后,main线程修改了run的值,并同步至主存,但是线程仍然从自己的工作内存的高速缓存中读取这个变量的值,结果永远是之前的值 - 怎么解决呢?
volatile(易变关键字),在run 前加上volatile关键字修饰,
- volatile修饰的变量读取每次都到主存中读取,总是读到最新数据
- 保证多个线程之间,一个线程对变量的修改,另外的线程可以看到,
- 可以保证可见性但是不能保证原子性,适用一个线程写多个线程读的情况
- 与synchronized相比是轻量级的,synchronized既可以保证原子性也可以保证可见性,重量级的
- 思考:在while中随意加入个输出语句,为什么会停下来循环?
println()方法底层加了synchronized
3.有序性
jdk5以上才真正可以禁用指令重排
举例
int num = 0;
boolean ready = false;
//线程1 执行此方法
public void actor1(I_Result r){
if(ready){
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r){
num = 2;
ready = true;
}
两个线程混合执行,可能产生多种结果…产生指令重排引起的(可以用jcstress进行测试)
怎么解决呢?
用volatile修饰的变量可以禁止指令重排
在同一个线程中,jvm会在不影响正确性的前提下,可以调整语句的执行顺序;在多线程下【指令重排】会影响正确性,例如著名的double-checked locking模式实现单例
public final class Singleton{
private Singleton(){}
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance(){
if(INSTANCE == null){//实例没创建,才会进入内部的synchronized代码块
synchronized (Singleton.class){
if(INSTANCE == null){//也许其他线程已经创建实例,所以再判断一次
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
特点:
- 懒加载
- 首次使用getInstance()加锁,后续使用无需加锁
- 但是多线程环境下,指令重拍,上面的代码有问题,加上volatile可以禁止指令重排
4.happens-before
针对成员变量或者静态变量
- 线程解锁m之前对变量的写,对于接下来对m加锁的其他线程对该变量读可见
- 线程对volatile变量的写,对接下来其他线程对该变量的读可见
- 线程start前对变量的写,对该线程开始后对该变量的读可见
- 线程结束前对变量的写,对其他线程得知它结束后的读可见
- 线程T1打断线程T2前对变量的写,对于其他线程得知T2被打断后对变量的读可见
- 对变量默认值的写,对其他线程对该变量的读可见
- 具有传递性