private static volatile boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int i = 0;
while (!stop){ //(1) 读
i++;
}
});
thread.start();
Thread.sleep(1000);
stop = true; //(2) 写
}
上面的代码,如果stop变量没有被volatile修饰的话,线程是不会被终止的,只有加上volatile,线程才会退出。
分析:
多线程环境下,读线程不能及时的获取到其他线程写入的最新的值,这就是所谓的可见性问题。
? 众所周知,CPU、内存和IO设备之间存在速度的不匹配,为了平衡三者之间的速度差异,最大化的利用CPU提升性能,硬件、操作系统、编译器等方面都做出了很多的优化
解决了处理器和内存的速度矛盾,但是会产生了新的问题,缓存一致性
? 由于每个线程都有自己的高速缓存,同一份数据可能被缓存到不同的CPU中,如果在不同的CPU中运行的不同线程看到同一份内存的缓存值不一样就会存在缓存不一致的问题。
? CPU层面的解决办法:
(1)总线锁
? 把CPU和主内存之间的通信锁住,锁定期间,其他处理器无法访问其他内存地址的数据。开销很大,显然不合适
(2)缓存锁
? 基于缓存一致性协议(MESI)实现,控制所得粒度。
? MESI协议
a),M(Modify):共享数据被修改了为Modify,也就是缓存的数据和主内存不一致
b),E(Exclusive):表示缓存的独占状态,数据只缓存在当前缓存,其他处理器没有。
c),S(Shared):数据被多个CPU缓存,且各个缓存与主内存数据一致
d),I(Invalid):表示缓存已经失效
? 在MESI协议中,每个缓存的缓存控制器不仅知道自己的读写操作,而且也监听其它Cache的读写操作。
? 对于MESI协议,从CPU读写角度来说会遵循以下原则:
? (1)CPU读请求:缓存处于M,E,S状态的都可以被读取,I状态CPU只能从主存中读取数据
? (2)CPU写请求:缓存处于M,E状态才可以被写,对于S状态的写,需要将其他CPU中缓存行置为无效才可写。
? MESI优化带来的可见性问题
? MESI的优化可能会导致CPU的乱序执行,这种乱序执行会带来可见性的问题。(假如当前修改的是CPU0,其他CPU线程简称为CPU1)过程就是CPU0引入了storebuff,将数据的修改执行放到storebuff,然后发送消息给CPU1,这时候CPU0可以继续执行接下来的代码,当storebuff收到CPU1线程的ack应答消息后,storebuff将修改的数据同步到缓存行,再同步到主内存当中。
? CPU层面的内存屏障
? 内存屏障就是将storebuffer中的指令写入主内存,从而使得其他访问同一共享内存的线程的可见性。
a),读屏障:可以看做是一定将数据从高速缓存中抹掉,从内存中读出来。保证读操作有序。
b),写屏障:可以看做是一定将数据写回内存,而不是写到高速缓存中。写屏障之前的指令的结果对屏障之后的读或者写是可见的。保证写操作有序
c),通用屏障:保证读写操作有序。
所有的CPU内存屏障(除了数据依赖屏障外)都隐含了编译器屏障(也就是使用CPU内存屏障后就无需再额外添加编译器屏障了)
? java内存模型,Java Memory Model,由前面分析得知,缓存及重排序导致了可见性的问题,JMM定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。它解决了CPU多级缓存、处理器优化、指令重排序导致的内存访问问题,保证了并发场景下的可见性问题。
? 简单来说,它提供了一些禁用缓存及禁止重排序的方法,如volatile,synchronized,final等
? 为了提高程序的运行性能,编译器和处理器都会对指令做重排序。
? 源代码 --> 1.编译器重排序 --> 2.指令重排序 --> 3.内存系统重排序 --> 最终执行的指令序列
? 1,编译器的重排序,编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。JMM提供了禁止特定类型的编译器重排序
? 2,处理器重排序(2和3),如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。JMM会要求编译器生成指令时,会插入内存屏障来禁止处理器重排序。
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad Barriers | Load1; LoadLoad; Load2 | 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载 |
StoreStore Barriers | Store1; StoreStore; Store2 | 确保Store1数据对于其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储 |
LoadStore Barriers | Load1; LoadStore; Store2 | 确保Load1数据的装载,之前于Store2及所有后续的存储指令刷新到内存 |
StoreLoad Barriers | Store1; StoreLoad; Load2 | 确保Store1数据对于其他处理器可见,之前于Load2及所有后续装载指令的装载,StoreLoad Barriesrs会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。 |
? 用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系 。happens-before关系本质上和as-if-serial语义是一回事。as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
原文:https://www.cnblogs.com/gaojf/p/12744832.html