什么是Java内存模型JMM?
java是跨平台的语言,但每个平台的 内存管理 是有差异的,为了屏蔽这些差异,就抽象出了一种概念JMM。
JMM主要体现在以下几个方面
- 原子性,保证指令不会受线程上下文切换影响
- 可见性,保证指令不会受cpu缓存影响
- 有序性,保证指令不会受cpu指令并行优化的影响
可见性
如下所示,一个线程判断stop是否为true,不为true则一直工作,由主线程更改这个值为true来停止t线程的工作,但这样是不起作用的
private static Boolean run = true;
@Test
public void testSee() throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
log.info("工作中。。。。");
}
log.info("停止工作");
}).start();
Thread.sleep(1000);
run = false;
}
JIT编译器会将主存中的变量复制到高速缓存中,每次取都是从这个 高速缓存 中取,即使主线程改了主内存中的值,t线程也看不到,要保证run变量的可见性,需要加volatile关键字。 jdk 1.8中可见性有所不同,测试上面代码是正常的,但最佳实践是都加上volatile关键字
可见性和原子性
volatile关键字只能保证一个线程修改后对其他线程可见,无法保证原子性,适用于一个线程写,多个线程读的场合,要保证原子性需要用线程安全的类如ConcurrentHashMap, Vector 或加锁。
有序性
JVM 会在不影响正确性的前提下,调整语句执行顺序。 volatile 可以保证有序性
先行发生原则(happens-before)
由于jvm为了优化会重排指令顺序,就有可能导致程序执行不正确,为了防止这种情况,指令重排需要满足happens-before原则。
happends-before的思想就是满足下面的条件,A就会优先于B发生。
happens-before总原则
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,
而且第一个操作的执行顺序排在第二个操作之前。 - 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。
如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
happends-before具体原则:
- 次序规则,一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作,按照总原则还会指令重排,但不影响结果
- 锁定规则,一个unLock操作先行发生于后面((这里的“后面”是指时间上的先后))对同一个锁的lock操作;
- volatile变量规则,对一个volatile变量的写操作先行发生于后面对这个变量的读操作,
前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后。 - 传递规则,如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
- 线程启动规则,Thread对象的start()方法先行发生于此线程的每一个动作
- 线程中断规则,对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终止规则,线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行。
- 对象终结规则,对象没有完成初始化之前,是不能调用finalized()方法的
volatile原理
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
也就是说volatile可以保证可见性和有序性。
volatile可见性和有序性是用内存屏障实现的,分为读屏障和写屏障
加了volatile的变量在赋值后会加入写屏障,之前的变量会同步到主存中,同时写屏障之前的代码不会重排到写屏障后面,保证了有序性
run = false;
//加入写屏障,之前的变量同步到主存
在读volatile变量前会加入读屏障,后面的数据都从主存中读取,同时能保证读屏障之后的代码不会重排到读屏障之前
//加入读屏障,加载主存中最新数据
while (run) {
log.info("工作中。。。。");
}