Java内存模型
JMM即java memory model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM体现在以下几个方面
· 原子性 - 保证指令不受到线程上下文切换的影响(之前的synchornized原理文章有介绍过)
· 可见性 - 保证指令不会受CPU缓存的影响
· 有序性 - 保证指令不会受CPU指令并行优化的影响
可见性
先看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止:
@Slf4j(topic = "c.TestDemo")
public class TestDemo {static boolean run = true;public static void main(String[] args) throws InterruptedException {new Thread(()->{while (run){}}).start();Thread.sleep(1000);log.debug("修改run");run = false;}
}
1.初始状态,t线程刚开始从主内存读取了run的值到工作内存。
2.因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率
3.1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决方法
volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。
volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {new Thread(()->{while (run){}}).start();Thread.sleep(1000);log.debug("修改run");run = false;
}
加锁也可以保证可见性
static boolean run = true;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {new Thread(()->{while (run){synchronized (lock){if (!run){break;}}}},"t1").start();Thread.sleep(1000);log.debug("停止t");synchronized (lock){run = false;}
}
但是由于synchronized需要给对象创建monitor比较重量,所以建议使用volatile
可见性&原子性
前一个例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况
比较一下之前我们将线程安全时举的例子:两个线程一个i++一个i–,只能保证看到最新值,不能解决指令交错
注意
synchronized语句块既可以保证代码块的原子性,也可以保证代码块的可见性。但缺点是synchronized是属于重量级操作,性能相对较低
如果前边示例死循环中加入System.out.println()不用volatile修饰符,线程t也能正确看到run变量的修改。
有序性
JVM会在不影响正确性的前提下,可以调整语句的执行顺序。
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;
可以看到,至于是先执行i还是先执行j,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是先给i赋值也可以是先给j赋值
这种特性称之为指令重排,多线程下指令重排会影响正确性。
指令重排的前提时,重排指令不能影响结果
//可以重排的例子
int a = 10;//指令1
int b = 20;//指令2
System.out.println(a+b);//不能重排的例子
int a = 10;//指令1
int b = a - 5;//指令2
为什么会产生指令重排呢?
事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的CPU指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每个指令都可以分为:取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这5个阶段
术语参考:
· instruction fetch(IF)
· instruction decode(ID)
· execute(EX)
· memory access(MEM)
· register write back(WB)
现代CPU支持多级指令流水线,例如支持同时执行 IF - ID - EX - MEM - WB 的处理器,就可以称为五级指令流水线。这时CPU可以在一个时钟周期内,同时运行五条指令的不同阶段,IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但他变相地提高了指令地吞吐率
指令重排序在多线程下的影响
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;
}
结果会出现四种情况
情况1: 线程1先执行,这时ready=false,所有进入else分支结果为1
情况2: 线程2限制性num=2,但没来得及执行ready = true,线程1执行,还是进入else分支,结果为1
情况3: 线程二执行到ready=true,线程1执行,结果为4
情况4:线程2执行ready=true,切换到线程1,进入if分支,相加为0,再切回线程2执行num=2
情况4这种情况就是指令重排的体现
这里只需在ready 变量前加上volatile关键字就行了(不在num属性上加是因为volatile关键字能确保之前的代码也不指令重排 )
happens-before
happens-before规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套总结,抛开以下happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
· 线程解锁m之前对变量的写,对于接下来对m加锁的其他线程对该变量的读可见
static int x;
static Object m = new Object();
new Thread(()->{synchronized(m){x = 10;}
},"t1").start();
new Thread(()->{synchronized(m){System.out.println(x);}
},"t2").start();
· 线程对volatile变量的写,对接下来其它线程对该变量的读可见
static int x;
static Object m = new Object();
new Thread(()->{synchronized(m){x = 10;}
},"t1").start();
new Thread(()->{synchronized(m){System.out.println(x);}
},"t2").start();
· 线程对volatile变量的写,对接下来其他线程对该变量的读可见
volatile static int x;
new Thread(()->{x = 10;
},"t1").start();
new Thread(()->{System.out.println(x);
},"t2").start();
· 线程start前对变量的写,对该线程开始后对该变量的读可见
static int x;
x = 10;
new Thread(()->{System.out.println(x);
},"t2").start();
· 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用t1.isAlive()或t1.join()等待它结束)
static int x;
Thread t1 = new Thread(()->{x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
· 线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过t2.interrupted或t2.isInterrupted)
static int x;
public static void main(String[] args){Thread t2 = new Thread(()->{while(true){if(Thread.currentThread().isInterrupted()){System.out.println(x);break;}}},"t2");t2.start();new Thread(()->{sleep(1);x = 10;t2.interrupt();},"t1").start();while(!t2.isInterrupted()){Thread.yield();}System.out.println(x);
}
· 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见。具有传递性,如果x hb-> y并且y hb-> z 那么有x hb-> z,配合volatile的防指令重排。
volatile static int x;
static int y;
new Thread(()->{y = 10;x = 20;
},"t1").start();
new Thread(()->{// x=20 对 t2可见,同时 y=10 也对t2可见System.out.println(x);
},"t2").start();