volatile关键字详解

可见性

CPU的高速缓存

当cpu运行程序时,先到主内存中读取数据,然后缓存到高速缓存中,运行完成之后cpu先会把结果写回告诉缓存,然后在同步到主内存。这样设计就减少了内存和cpu之间的速度差异,cpu的速度大概是内存的1000倍以上,如果每次都去内存中取数据,那么将会极大的浪费cpu的性能。
高速缓存又分为L1,L2,L3缓存,L1和L2是cpu私有的,L3
是多个cpu共有的,那么多个cpu对同一个资源进行处理时就会存在缓存一致性的问题。
例如cpu0从主内存中加载变量i,然后对i进行加加操作,cpu1也是做同样的事情,那么cpu1可能读取到的i可能并不是cpu0操作之后的值,那么最后的运行结果是小于等于2的,然而这样的结果并不是我们所期望的,这就是缓存不一致带来的问题。
那么对于一致性的问题就出现了两张解决方案:总线锁和缓存锁。
总线锁:cpu和内存通信的总线上加锁(相当于锁住整个内存),这样会导致串行话运行,效率较低
缓存锁:相当于总线锁来说,它是在共享变量上加锁,对于总线锁来说大大的减少了锁的粒度。
缓存锁的实现是通过缓存一致性协议实现的,对于不同的操作系统实现的方式不一样,对于常见的x86来说使用的MESI协议。
MESI协议:是一种表示共享变量的一种状态的描述,M是modify状态,E是exclusive,S是shared共享状态,I是invalid状态。

当两个cpu0和cpu1都缓存了i时,并未对i进行任何操作时,此时i属于share共享状态(表示变量被多个cpu缓存了,且和主内存保持一致),当cpu0改变i的值时,cpu0中i就变成了modify状态,cpu1中缓存的i就会变成invalid失效状态。如果只有cpu0缓存了i,且i的值和主内存保持一致,则此时i是exclusive独占状态。多个cpu之间是通过嗅探协议来感知自身缓存变量的状态
状态图:
share
在这里插入图片描述
modify和invalid
在这里插入图片描述
exclusive独占
在这里插入图片描述
MESI协议也会带来一些性能浪费的问题,如下图中的阻塞过程
在这里插入图片描述
为了解决发送invalid失效消息在得到回复之前这个时间段的则塞呢?从而引入了storebuffer,cpu0改变i的值时会把值写入storebuffer,然后由storebuffer同步到缓存,再到内存。而cpu把值写到storebuffer以后就认为它已经修改i的成功了,并且也已经同步到主内存中了,那么cpu的阻塞时间将变得非常短,不在需要则塞了,从而可以继续执行后续的指令。
storebuffer的引入之后,读取数据的顺序变为cpu—> storebuffer---->cpu缓存—>内存,同时似乎也没有解决的可见性问题,依然会存在短暂的可见性问题。因为cpu把值写入到storebuffer之后并不知道何时会被写入到内存中,这段时间内就依然存在可见性的问题。

value = 0
void cpu0(){
	value = 10; //s -> m 把i改变后的值写入到storebuffer中,
	//storebuffer再通知其他缓存行i失效,再写入缓存在到内存
	isfinished = true; //exclusive
}

void cpu1(){
	if(isfinished){ //true
		assert value == 10; //false
	}
}

上面一段代码中如果isfinished处于独占状态,当value写入到storebuffer以后,cpu并不会等待storebuffer写入到内存之后再去执行下一行代码,因为isfinished是独占状态它并不需要通知其他缓存行失效这不操作,那么它同步到内存的时间可能会快鱼value同步到内存的时间快,那样就会导致cpu1方法的执行结果并不是预期值,这就叫做cpu的乱序执行,或者重排序(相当于isfinished=true先执行),从而存在短暂的可见性问题。此时从硬件层面已经无法解决此种问题了。

内存屏障

因为硬件层面无法感知真正的代码逻辑来解决短暂可见性这一问题,那么cpu层面就提供了一个内存屏障的指令

value = 0
void cpu0(){
	value = 10; 
	storeMemoryBarrier(); //伪代码,写屏障
	isfinished = true; 
}

void cpu1(){
	if(isfinished){ 
		loadMemoryBarrier();//读屏障
		assert value == 10; 
	}
}

写屏障:会强制把value的值更新到主内存中,再执行下一行代码
读屏障:读取value值时,强制cpu去主内存中读取最新的值。
全屏障:就是写屏障和读屏障同时使用。

volatile --> lock(汇编指令,缓存锁)–>内存屏障
然而不同的操作系统内存屏障的实现也是不同,那我们写代码并不可能针对不同的操作系统而实现不同的代码,java的运行是和平台无关的,那么它做了什么样的设计来屏蔽因操作系统不同带来的差异呢?java是引入了JMM模型来 屏蔽这层差异的。

JMM内存模型

jmm提供了语言层面的和cpu层面的内存屏障,通过volatile修饰的变量编译之后会存在acc_volatile的标志
在这里插入图片描述
源码在存储变量时会判断是否有被volatile修改,如果被volatile修饰就会加上内存屏障,而OrderAccess的storeload的实现是根据平台的不同实现的方式不同,从而屏蔽了平台所带来的差异性
在这里插入图片描述
linux x86的实现:
在这里插入图片描述
提供了四种屏障如上图
load1 loadload load2 这就意味着load2操作一定实在load1完成之后再去执行
store storeload load 这相当于全屏障,保证了load操作一定能读到store修改之后的值。

还提供了一种happen-before的指定,和storeload的功能差不多,那么哪些操作会使得happen-before的出现呢
1.线程的start方法之前,如果线程方法里用到了某个变量,那么这个变量在start方法之前做过的改变对于线程运行时一定是可见。

public class Demo {

    private static int i = 0;

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println(i);
        });
        i = 10;
        thread.start();
    }
}

就是说这个结果肯定时10,相当于1 =10 happen-before threa.start()

2.线程的jion方法之后,如果线程里改变了某个变量的值,那么jion操作之后的变量肯定是改变了的

public class Demo {

    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            i = 100;
        });
        thread.start();
        thread.join();
        System.out.println(i);
    }
}

结果是100;
3.监视器规则,在一个线程改变i = 10这个操作之后释放锁,另一个线程得到锁然后执行代码,那么得到的i的值一定是10。

    private static int i = 0;
    
    private static void sync(){
        synchronized (Demo.class){
            i = 10;
        }
    }

volatile,synchronized和final都会禁止内存进行重排序,volatile还会使得修饰变量强制写到内存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值