并发编程系列(三):重排序和可见性问题

一、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,后续获得锁的线程


 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值