认识volatile关键字
1.在Java程序中使用volatile关键字和不加关键字的区别
package com.pattern.volatileTest;
/**
* Created by Mr.chne on 2019/10/26.
*/
public class VolatileTest {
volatile static boolean stop=false;
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
int i=0;
while (!stop){
i++;
}
});
thread.start();
System.out.println("Thread1 start!");
Thread.sleep(1000);
stop=true;
}
}
2.volatile的作用
volatile在多环境处理器下实现数据的可见性;
2.1.可见性
在多线程的环境下,由于读操作和写操作发生在不同的线程中的时候,会出现数据不一致问题,读线程不能及时的读取到写线程的写入最新的值,这就是可见性;为了实现可见性,则volatile实现了这一机制;
3.volatile如何保证可见性?
可以通过hsdis工具来查询这段程序运行的结果,在运行的代码中,设置jvm参数如下 【-server -Xcomp -XX:+UnlockDiagnosticVMOptions XX:+PrintAssembly
XX:CompileCommand=compileonly(替换成实际 运行的代码) ;
可以在运行结果中查看到有Lock成员变量,Lock指令可以基于2中方式实现可见性;(总线锁和缓存锁)
3.硬件方面了解可见性
计算机组成部分:CPU,内存,I/O设备
为了提升计算机的性能,则添加了cpu的高速缓存(L1/L2/L3)
3.1 cpu的高速缓存
由于计算机的存储设备与处理器的运 算速度差距非常大,所以现代计算机系统都会增加一层读 写速度尽可能接近处理器运算速度的高速缓存来作为内存 和处理器之间的缓冲:将运算需要使用的数据复制到缓存 中,让运算能快速进行,当运算结束后再从缓存同步到内 存之中。
通过高速缓存解决了处理器与内存的速度问题,也带来了缓存一致性问题;
3.2 缓存一致性问题?
每个cpu的处理过程是将主内存的数据备份到高速缓存中,cpu在计算时候,直接在高速缓存中读取数据在进行计算然后在写入缓存中在同步到主内存;由于有多个cpu,则每个线程运行在不同的cpu内,都有自己独立的高速缓存,同一份数据可以被缓存在多个cpu中,如果在不同 CPU 中运行的不同线程看到同一份内存的缓存值不一样就会存在缓存不一致的问 题。
为了解决缓存不一致问题,则cpu的策略提供了许多方法。
主要有总线锁和缓存锁;
总线锁:
总线锁,简单来说就是,在多cpu下,当其中一个处理器 要对共享内存进行操作的时候,在总线上发出一个LOCK# 信号,这个信号使得其他处理器无法通过总线来访问到共 享内存中的数据,总线锁定把CPU和内存之间的通信锁住 了,这使得锁定期间,其他处理器不能操作其他内存地址 的数据,所以总线锁定的开销比较大,这种机制显然是不 合适的
缓存锁:
最好的方法就是控制锁的保护粒度,我们只 需要保证对于被多个 CPU 缓存的同一份数据是一致的就 行。所以引入了缓存锁,它核心机制是基于缓存一致性协 议来实现的。
3.3 缓存一致性协议
为了 达到数据一致性,需要各个处理器遵循一些协议,在读写根据一些协议来操作,常见的协议有MSI,MESI,MOSI等;最常见的就是MESI协议;
3.4 MESI协议
1.M(Modify) 共享数据只缓存在当前CPU缓存中,并且可以被修改,也就是缓存的数据和主内存中的数据不一致
2.E(Exclusive) 表示缓存独占,数据只缓存在在当前cpu中,并且没有被修改;
3.I(Invalid) 表示缓存已失效;在MESI协议中每个缓存的控制器不仅知道自己的读写操作,而且可以监听其他的cache的读写操作
4.S(Shared) 表示数据被多个CPU缓存,并且各个缓存中的数据和主内存中的数据一致
对于 MESI 协议,从 CPU 读写角度来说会遵循以下原则:
CPU读请求:缓存处于M、E、S状态都可以被读取,I状 态CPU只能从主存中读取数据 CPU写请求:缓存处于M、E状态才可以被写。对于S状 态的写,需要将其他CPU中缓存行置为无效才可写 使用总线锁和缓存锁机制之后,CPU对于内存的操作大概 可以抽象成下面这样的结构。从而达到缓存一致性效果
由于CPU 高速缓存的出现使得 如果多个cpu同时缓存了 相同的共享数据时,可能存在可见性问题。也就是CPU0修 改了自己本地缓存的值对于 CPU1 不可见。不可见导致的 后果是 CPU1 后续在对该数据进行写入操作时,是使用的 脏数据。使得数据最终的结果不可预测。 很多同学肯定希望想在代码里面去模拟一下可见性的问题, 实际上,这种情况很难模拟。因为我们无法让某个线程指 定某个特定 CPU,这是系统底层的算法, JVM 应该也是 没法控制的。还有最重要的一点,就是你无法预测CPU缓 存什么时候会把值传给主存,可能这个时间间隔非常短, 短到你无法观察到。最后就是线程的执行的顺序问题,因 为多线程你无法控制哪个线程的某句代码会在另一个线程 的某句代码后面马上执行。 所以我们只能基于它的原理去了解这样一个存在的客观事
实
了解到这里,大家应该会有一个疑问,刚刚不是说基于缓 存一致性协议或者总线锁能够达到缓存一致性的要求吗? 为什么还需要加 volatile 关键字?或者说为什么还会存在 可见性问题呢?
3.5 MESI 优化带来的可见性问题
各个cpu缓存行的状态是通过消息机制来传递的,如果cpu0对一个缓存中共享的变量做修改的话,首先需要发送一个失效消息给其他缓存该数据的cpu,并且要等到他们的确认回执。cpu0在这段时间处理阻塞状态,为了避免cpu资源浪费,则引入一个storeBfferes.
cpu0只需要写入共享数据时,只要把数据写入storebuffer中
同时发送一个invalidate,然后去处理其他指令。当收到其他所有CPU发送了invalidate acknowledge消息 时,再将 store bufferes 中的数据数据存储至 cache line 中。最后再从缓存行同步到主内存。
这种优化会出现两个问题:
1.数据什么时候提交不确定,因为需要等到其他cpu给回复才行才进行同步数据,这里的操作是异步的。
2.引入storebuffer后,cpu会先尝试从storebuffer中读取数据,如果 storebuffer 中有数据,则直接从 storebuffer中读取,否则就再从缓存行中读取 ;
例如
exeToCPU0和exeToCPU1 分别在两个独立的cpu上运行,假如 CPU0 的缓存行中缓存了 isFinish 这个共享变量,并 且状态为(E)、而Value可能是(S)状态。 那么这个时候,CPU0在执行的时候,会先把value=10的 指令写入到storebuffer中。并且通知给其他缓存了该value 变量的CPU。在等待其他CPU通知结果的时候,CPU0会 继续执行isFinish=true这个指令。
而因为当前CPU0缓存了isFinish并且是Exclusive状态,所
以可以直接修改 isFinish=true。这个时候 CPU1 发起 read 操作去读取isFinish的值可能为true,但是value的值不等 于10。
这种情况我们可以认为是CPU的乱序执行,也可以认为是 一种重排序,而这种重排序会带来可见性的问题
所以在 CPU 层面提供了 memory barrier(内存屏障)的指 令,从硬件层面来看这个 memroy barrier就是CPU flush store bufferes中的指令。软件层面可以决定在适当的地方 来插入内存屏障。
内存屏障
内存屏障就是将 store bufferes 中的指令写入到内存,从 而使得其他访问同一共享内存的线程的可见性。 X86的memory barrier指令包括lfence(读屏障) sfence(写 屏障) mfence(全屏障)
Store Memory Barrier(写屏障) 告诉处理器在写屏障之前 的所有已经存储在存储缓存(store bufferes)中的数据同步 到主内存,简单来说就是使得写屏障之前的指令的结果对 屏障之后的读或者写是可见的 Load Memory Barrier(读屏障) 处理器在读屏障之后的读 操作,都在读屏障之后执行。配合写屏障,使得写屏障之前 的内存更新对于读屏障之后的读操作是可见的 Full Memory Barrier(全屏障) 确保屏障前的内存读写操作 的结果提交到内存之后,再执行屏障后的读写操作 有了内存屏障以后,对于上面这个例子,我们可以这么来 改,从而避免出现可见性问题
内存屏障的作用可以通过防止cpu对内存的乱序访问来保证共享数据在多线程并行的执行下是可见的,但是这个屏障怎么来加呢?回到最开始我们讲 volatile 关 键字的代码,这个关键字会生成一个Lock的汇编指令,这 个指令其实就相当于实现了一种内存屏障
这个时候问题又来了,内存屏障、重排序这些东西好像是 和平台以及硬件架构有关系的。作为Java语言的特性,一 次编写多处运行。我们不应该考虑平台相关的问题,并且 这些所谓的内存屏障也不应该让程序员来关心。
4.JMM
Java 内存模型底层实现可以简单的认为:通过内存屏障 (memory barrier)禁止重排序,即时编译器根据具体的底层 体系架构,将这些内存屏障替换成具体的 CPU 指令。对 于编译器而言,内存屏障将限制它所能做的重排序优化。 而对于处理器而言,内存屏障将会导致缓存的刷新操作。 比如,对于 volatile,编译器将在 volatile 字段的读写操作 前后各插入一些内存屏障。
JMM如何解决可见性有序性问题
,JMM提供了一些禁用缓存以及进制重排序的方 法,来解决可见性和有序性问题。这些方法大家都很熟悉: volatile、synchronized、final;
JMM如何解决一致性问题
2 和 3 属于处理器重排序。这些重排序可能会导致可见性 问题。
编译器的重排序,JMM提供了禁止特定类型的编译器重排 序。 处理器重排序,JMM会要求编译器生成指令时,会插入内
存屏障来禁止处理器重排序
JMM 层面的内存屏障
为了保证内存可见性,Java 编译器在生成指令序列的适当
位置会插入内存屏障来禁止特定类型的处理器的重排序, 在JMM中把内存屏障分为四类
HappenBefore
它的意思表示的是前一个操作的结果对于后续操作是可见 的,所以它是一种表达多个线程之间对于内存的可见性。 所以我们可以认为在JMM中,如果一个操作执行的结果需 要对另一个操作课件,那么这两个操作必须要存在 happens-before关系。这两个操作可以是同一个线程,也 可以是不同的线程
JMM 中有哪些方法建立 happen-before 规则
1.程序的顺序规则
- 一个线程中的每个操作,happens-before于该线程中的 任意后续操作; 可以简单认为是as-if-serial。 单个线程
中的代码顺序不管怎么变,对于结果来说是不变的 顺序规则表示 1 happenns-before 2; 3 happensbefore 4
2.volatile变量规则
对于修饰volatile的写操作一定发生在happen-before后对volatile变量的读操作
根据volatile规则,2 happens before 3
3. 传递性规则,如果 1 happens-before 2; 3happensbefore 4; 那么传递性规则表示: 1 happens-before 4;
4. start规则,如果线程A执行操作ThreadB.start(),那么线 程 A 的 ThreadB.start()操作 happens-before 线程 B 中
的任意操作
2. join规则,如果线程A执行操作ThreadB.join()并成功返 回,那么线程 B 中的任意操作 happens-before 于线程 A从ThreadB.join()操作成功返回。
package com.pattern.volatileTest;
/**
* Created by chenli on 2019/10/26.
*/
public class VolatileTest {
volatile static boolean stop=false;
static int x;
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
x=100;
});
thread.start();
thread.join();
//阻塞主线程
System.out.println(x);
}
}
- 监视器锁的规则,对一个锁的解锁,happens-before于 随后对这个锁的加锁