一、volatile关键字作用
eg.代码演示有无加volatile关键字的显示结果
public class App {
public volatile static boolean stop=false;
public static void main( String[] args ) throws InterruptedException {
Thread t1=new Thread(()->{
int i=0;
while(!stop){ // condition 不满足
i++;
}
});
t1.start();
Thread.sleep(1000);
stop=true; // true
}
}
加了volatile,当stop值改变,程序会立即停止;而未加volatile情况,程序没有立即停止,说明stop值的改变没有被立即读取到。
解决问题:在多线程情况下,若读和写发生在不同环境中,则可能会出现读线程不能及时读取到其他线程写入的最新值。
二、硬件层面看可见性问题发展
计算机中CPU、内存和设备I/O操作执行速度不一样,速度上CPU>内存>设备I/O
1.高速缓存
为了解决处理器与内存交互过程中,比如:读取运算数据,存储计算结果这些I/O操作,运算速度的差距,在内存与CPU间增加了一层读写速度接近CPU的高速缓存来作为缓冲。
带来问题:缓存一致性
在多核CPU中,每个线程可能运行在不同CPU中,各自拥有相同的一份高速缓存。若不同CPU中的不同线程看到同一份内存的缓存值不一样,可能会有缓存一致性问题。
解决:总线锁、缓存锁
2.总线锁、缓存锁
总线锁,在多CPU中,其中一个处理器要对共享数据操作时,在总线上发出一个lock信号,关闭其他处理器与内存间通信
有点类似于java层面的synchronized,使得获取共享数据操作串行执行。
问题:控制粒度较大,带来较大的开销
优化:缓存锁,只要保证多个CPU缓存的同一份数据是一致的就行
MESI缓存一致性协议
M(Modify),修改状态,当前CPU独占,缓存数据与内存数据不一致
E(Exclusive),当前CPU独占,其余CPU下的线程无法访问该块缓存,缓存数据和内存数据一致
S(Share),共享状态,不同CPU下的不同线程,缓存数据与内存一致
I(Invalid),失效状态,缓存数据无效
可见性问题本质:CPU0修改了本地缓存的值对于CPU1不可见,导致后续CPU1对该数据执行写入操作时,用到的数据是脏数据,使得最终结果不可测。
MESI带来的问题:
各个CPU缓存行之间状态的改变是通过消息传递进行的。当CPU0要对缓存中的共享变量执行写入操作时,会先发送一个invalidate消息给其他缓存了该份数据的CPU,并等到它们确认回执,这个过程CPU0处于阻塞状态,为了避免阻塞带来的资源浪费,CPU中引入了store buffers。
eg.
3.store buffers
执行写入操作时,发送失效消息给其他线程,并把写入数据存入store buffers中,然后执行其他操作;当其他线程返回invalid acknowledge消息时,再将store buffers中存储的数据存储至缓存行cache line中,最后再从缓存行同步到主内存。
问题:在等待确认回执和写入store buffers这个过程是异步的。
eg.CPU执行顺序不一致导致的结果不确定问题
value = 3;
void exeToCPU0() {
value = 10;
isFinish = true;
}
void exeToCPU0() {
if(isFinish) {
asser value == 10;
}
}
分析:
- exeToCPU0和exeToCPU0分别在两个独立的CPU中执行,假如CPU0的缓存行中缓存了isFinish这个共享变量,并且状态为E,而value可能是S状态。
- 那么此时CPU0会先把value=10的指令写入到store buffers,并且通知其他缓存了该value的CPU。在等待其他CPU通知结果时,CPU0会继续执行后续isFinish=true这个指令。
- 而因为当前CPU0缓存了isFinish,并且为Exclusive状态,所以可直接修改isFinish=true。这个时候CPU1发起read操作读取isFinish的值可能是true,但是value的值不等于10。
- 这种情况可认为是CPU乱序执行,也可认为是一种重排序,而这种重排序会带来可见性问题。
问题:当一个CPU操作还没等到确认回执时,接下来的CPU操作拿到的还是旧值。
解决:引入了内存屏障,解决执行顺序和指令重排序问题。
4.CPU层面内存屏障
内存屏障,将store buffers中的数据写入内存,并对其他访问同一共享内存数据的线程具有可见性,对数据变化的可见性。
store memory barrier(写屏障),在写屏障前的指令的结果对屏障之后的读、写是可见的。
load memory barrier(读屏障),读屏障前的写操作对于内存的更新,对于屏障后的读操作是可见的。
full memory barrier(全屏障),确保屏障前的读写操作提交到内存后,再执行全屏障后的操作。
eg.
value = 3;
void exeToCPU0() {
value = 10;
storeMemoryBarrier(); // 伪代码,插入写屏障,使得value=10这个值强制写入主内存中
isFinish = true;
}
void exeToCPU0() {
if(isFinish) {
loadMemoryBarrier(); // 伪代码,插入读屏障,使得CPU1从主内存中获得最新值
asser value == 10;
}
}
三、JMM(Java Memory Model)
JMM属于语言级别的抽象内存模型,也可以理解为是对硬件模型的抽象,定义了共享内存中多线程读写操作的规范
1.JMM抽象模型
JMM抽象模型分为主内存、工作内存
主内存,存放实例对象、静态数据等存放于堆内存中的变量。
工作内存,线程独占,线程对变量的所有操作必须在工作内存中进行,不能直接读写主内存变量。线程间变量传递基于主内存来完成。
2.JMM的作用
JMM提供了合理的禁用缓存以及禁止重排序的方法。解决:可见性和有序性问题
解决可见性有序性问题:
volatile、synchronized、final等。
解决重排序问题:
重排序,其实就是指令的执行顺序。
为了提高程序性能,编译器和处理器会对指令做重排序。从源代码到最终执行的指令可能经过3种重排序。
编译器重排序,JMM提供了禁止特定类型编译器的重排序。
处理器重排序,JMM会要求编译器生成指令时,插入内存屏障来禁止处理器重排序。
并不是所有的指令都会重排序,比如有依赖关系的指令,重排序后对结果发生改变,这种不会做重排序优化,这种规则也叫as-if-serial
eg.
int a=1;
int b=2;
int c=a+b;
四、happens-before
表示前一个操作的结果对于后续操作时可见的。
1.程序顺序规则,也即as-if-serial
不管代码顺序怎么变,不影响最终结果
2.volatile规则
volatile修饰变量的写操作happens-before后续对该变量的读操作
3.传递性规则
操作1happens-before操作2,操作2happens-before操作3,则操作1happens-before操作3
4.start规则
主程序内在启动线程t1的start方法前,对于共享变量的修改happens-before线程t1中的任意操作
5.join规则
调用了join方法的线程happens-before其他线程的执行顺序
eg.
public class JoinRule {
static int x=0;
public static void main(String[] args) throws InterruptedException {
/*Thread t1=new Thread(()->{
x=100;
});
t1.start();
t1.join();
System.out.println(x);*/
Thread t1=new Thread(()->{
System.out.println("t1");
// 执行的结果对于主线程可见
});
Thread t2=new Thread(()->{
System.out.println("t2");
});
Thread t3=new Thread(()->{
System.out.println("t3");
});
t1.start();
// 阻塞主线程 wait/notify
// 等到阻塞释放
// 获取到t1线程的执行结果.
t1.join();
t2.start();
t2.join(); // 建立一个happens-bebefore规则
t3.start();
}
}
6.监视器的锁规则
锁修饰的方法或代码块的操作happens-before,后续获得锁的线程