深入学习重点分析java基础---第二章:java并发 volatile
1.缓存一致性
由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
正是因为有每个cpu都有自己的高速缓存所以引发了缓存一致性问题
如果一个变量在多个高速缓存中都存在,那么由于高速缓存之间数据不共享,势必会存在数据不一致的问题,为了解决缓存一致性问题,硬件层面来说通常有以下2种解决方法:
1)通过在总线加LOCK#锁的方式
2)通过缓存一致性协议
早期由于cpu和其他硬件都是通过总线进行通信,所以早期cpu通过在总线上加LOCK锁的方式来避免缓存不一致,LOCK类似于悲观锁,一旦有cpu需要访问变量就要在总线加锁阻塞其他cpu访问其他任何硬件包括内存,这样大大的降低的cpu的执行效率。
由于LOCK的效率太低所以出现了很多缓存一致性协议,其中最著名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。
MESI协议定义了缓存行的四种状态
modified(修改):缓存块已经被修改,必须被写回主存,其他处理器不能再缓存这个块
exclusive(互斥):缓存块还没有被修改,且其他处理器不能装入这个缓存块
share(共享):缓存块未被修改,且其他处理器可以装入这个缓存块
invalid(无效):缓存块中的数据无效
MESI核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
2.嗅探机制
每个处理器通过嗅探在总线上传播的数据来检查缓存的值是否有效,如果发现自己缓存行的内存地址被修改,就会将当前缓存行置位无效,当处理器需要对无效缓存行进行操作的时候会从主内存中重新读取到高速缓存中。
3.总线风暴
java中使用unsafe类作为实现cas的基石,cas底层由cpp代码调用汇编指令实现,如果是多核cpu则使用 lock cmpxchg指令,单核则compxch指令。
如果短时间内产生大量cas操作再加上volatile的嗅探机制会不断占用总线带宽,就会产生总线风暴,总线风暴会延迟其他线程的执行,甚至可能会延迟锁的释放。
4.流量风暴
缓存一致性协议存在的一个最大的问题是可能引起缓存一致性流量风暴,之前我们看到总线在同一时刻只能被一个处理器使用,当有大量缓存被修改,或者同一个缓存块一直被修改时,会产生大量的缓存一致性流量,从而占用总线,影响了其他正常的读写请求
一个最常见的例子就是如果多个线程对同一个变量一直使用CAS操作,那么会有大量修改操作,从而产生大量的缓存一致性流量,因为每一次CAS操作都会发出广播通知其他处理器,从而影响程序的性能。
5.伪共享
高速缓存都是由缓存行组成的,缓存系统以缓存行为单位存储。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。通常有两种方式解决伪共享问题。
1)使用缓存行填充注解避免伪共享@sun.misc.Contended
2)JVM参数 -XX:-RestrictContended
6.指令重排序
为了提高性能编译器和处理器经常会对既定的代码执行顺序进行指令重排序
1)编译器优化重排序
2)指令级并行重排序
3)内存系统重排序
volatile通过内存屏障来防止指令重排序
StoreLoad 屏障是一个全能型的屏障,它同时具有其他三个屏障的效果。所以执行该屏障开销会很大,因为它使处理器要把缓存中的数据全部刷新到内存中。
volatile 读 / 写插入内存屏障规则:
在每个 volatile 读操作的后面插入 LoadLoad 屏障和 LoadStore 屏障。
在每个 volatile 写操作的前后分别插入一个 StoreStore 屏障和一个 StoreLoad 屏障。也就是说,编译器不会对 volatile 读与 volatile 读后面的任意内存操作重排序;编译器不会对 volatile 写与 volatile 写前面的任意内存操作重排序。
7.as-if-serial
as-if-serial即不管怎么重排序,单线程下程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial。
8.happens-before
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
1)程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
2)监视器锁规则:对一个监视器锁的解锁,happens-before 于随后对这个监视器锁的加锁。
3)volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
4)传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
5)start() 规则:Thread.start() 的调用会 happens-before 于启动线程里面的动作。
6)join() 规则:Thread 中的所有动作都 happens-before 于其他线程从 Thread.join() 中成功返回。
这里特别说明一下,happens-before 规则不是描述实际操作的先后顺序,它是用来描述可见性的一种规则。从 happens-before 的 volatile 变量规则可知,如果线程 A 写入了 volatile 修饰的变量 V,接着线程 B 读取了变量 V,那么,线程 A 写入变量 V 及之前的写操作都对线程 B 可见