java 高性能并发一之 volatile

volatile是java虚拟机提供的最轻量级的同步机制

一,它的作用主要有两个:

1.保证此变量对所有线程的可见性。

2.禁止指令重排序优化

 

“可见性”是指当一条线程修改了这个变量的值,新值对于其它线程来说是可以立即得知的。

volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。而普通变量的值在线程间传递均需要通过主内存来完成。例如,线程A修改一个普通变量的值V,然后向主内存进行回写,另外一条线程B在线程A回写完成之后再从主内存进行读取操作之后,新变量值V2才会对线程B可见。

(我们可以理解为volatile是 变量<--->主内存 ,而普通变量时 变量<--->线程工作内存<--->主内存)

这里重点讲一下禁止指令重排序的原理。

首先,什么是指令重排序呢?

在编译器运行时优化时,为了使得jvm内部的运算单元能被充分利用,编译器可能会对输入代码进行乱序执行优化,编译器会在计算之后将乱序执行的结果重组,保证该结果与执行顺序的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序,因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序保证。

volatile禁止指令重排序的原理:

1. lock addl $ 0x0 ,(%esp)

对被volatile修饰的变量进行操作,编译后,发现比普通变量的编译 lock addl $ 0x0 ,(%esp),这是一个空操作,但这不重要,重要的是lock前缀,(它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU或者别的内核无效化(Invalidate)其Cache),这种操作(“空操作”)相当于对Cache中的变量做了一次“store”和“write”操作,也就是变量改变后的值并没有在工作空间停留,而直接写入到了主内存。通过这样一个操作,使得volatile变量的修改对其它CPU立即可见。

2. 指令重排序的前提--正确的指令依赖

指令重排序是指CPU采用了允许将多条指令不按照规定的顺序分开发送给各相应电路单元处理。但并不能指令任意重排,CPU需要能正确处理指令依赖情况以保障程序能得出正确的执行结果。例如,指令1对A进行+1操作,指令2对A进行*2操作,指令3对B进行-4操作,那么指令1根2直接是有依赖的,因为1和2对A操作的先后顺序是会影响A最终的结果的,而3则跟1,2都没有依赖,所有指令3可以被重排放在1之前,2之后。1,2之间都可以。因此,lock addl $ 0x0 ,(%esp)指令把修改同步到内存时,意味着所有之前的操作(修改,赋值..)都已执行完成,这样便形成了“指令重排无法越过内存屏障”的效果。

 

二,volatile 写-读建立的 happens before 关系

happens-before 规则中有这么一条:
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

happens-before的这个规则会保证volatile写-读具有如下的内存语义:

volatile写的内存语义:

当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。

volatile读的内存语义:

当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

 

三,volatile的优点和缺点

1.优点

volatile变量读操作的性能消耗很低,与普通变量几乎没有什么差别。但是写操作则慢一些,因为它需要在代码中插入许多内存屏障来保证处理器不发生乱序执行,但是即便如此,大多数场景下volatile的总开销任然要比锁低。

2.缺点

频繁更改时,大量的写入反而可能会降低了性能。

 

四,使用

1.什么时候用volatile呢?(volatile最常用的地方之一就是DCL了)

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。

1.运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。

2.变量不需要与其它的状态变量共同参与不变约束。

我个人的理解是:

1.就是当多多线程环境下运算的时候不会改变volatile变量的值,它只是作为一个参与变量,本身的值不改变。

如:

        public static final int B = 10 ;
	
	public static volatile int C ;
	
	public static void add(){
		C ++ ;
	}

该情况下运算结果依赖于C的当前值,此时该程序并不是线程安全的。

可以通过synchronized

        public static final int B = 10 ;
	
	public static int C = 0 ;
	
	public static synchronized void add(){
		C ++  ;
	}

或者原子类

        public static final int B = 10 ;
	
	public static AtomicInteger C = new AtomicInteger(0) ;
	
	public static void add(){
		C.incrementAndGet() ;
	}

2.第二条规则则是,当volatile变量跟一个普通变量同时操作的情况。

此时C+=B 这条语句的代码仍然是可能会发生重排序的。

解决办法:对其加锁

        public static int B = 10 ;
	
	public static int C = 0 ;
	
	public static synchronized void add(){
		C += B  ;
	}

注意:此处即使使用两个原子类B,C而不加锁仍然是线程不安全的。

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值