java--volatile关键字

为什么引入缓存

程序在执行时是通过CPU指令来执行的,当数据读取和写入时,涉及到访问主存即物理内存,但由于CPU执行速度很快,而从内存中读取数据和写入数据速度很慢,为了解决这种速度上的差异,提高指令运行速度,就引入了缓存。
缓存一致性

缓存不一致问题

初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。
  最终结果i的值是1,而不是2

缓存一致性协议

所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线:缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个CPU缓存可以读写内存)。
CPU缓存不仅仅在做内存传输的时候才与总线打交道,而是不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。所以当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。只要某个处理器一写内存,其它处理器马上知道这块内存在它们的缓存段中已失效。如果需要读或写,会去内存读取最新的值。
简单来说就是CPU通过总线来得知其他CPU缓存的状态,在做什么,并根据这个来操作改变自己的缓存的状态,以保证能够读到最新的值。

并发编程三要素

原子性:指一个操作不可以再分割,要么一次执行成功,要么失败。比如i=1是原子操作,但a++就不是。

常见的原子操作:

基本类型数据的读取和赋值,赋值是赋给变量。
引用赋值。
原子类Atomtic类

可见性:指一个线程修改共享变量后内存的值,对其他线程是可见的。
有序性:程序执行顺序按照代码先后顺序执行。单线程内这个线程执行代码是有序的,但以这个角度去看其他线程的所有操作都是无序的。也就是指令重排序的问题。

JMM内存模型

JMM主要是为了用来屏蔽各个硬件平台及操作系统的访问内存的差异,它定义了程序变量访问的规则,是一种抽象的规范。它规定所有的变量都是存储在主内存中,而每个线程都有一份自己的工作内存(类似缓存),线程对每个变量的写操作都必须在自己的工作内存中进行,不能直接在主内存中操作。同时,线程不能去访问其他线程的工作内存。(JMM也是存在缓存一致性,重排序的问题)

Volatile作用

Volatile对可见性的保证
volatile修饰的变量,当一个线程在内存修改共享变量时,修改后会立即更新到主内存。这个写操作会使其他线程的工作内存无效。(缓存一致性协议:CPU发出信号传到总线上,告诉其他CPU我要修改这个共享变量了,其他CPU是一直在嗅探总线的,他们收到这个信号就会将自己的缓存行置为无效)
Volatile对有序性的保证

什么是重排序?

重排序是编译器为了优化程序性能对指令进行的一种排序。
重排序的原则:数据之间存在依赖关系不能重排序。
不论怎么重排序,单线程下的执行结果不能被改变。
重排序会导致多线程环境下执行结果出错。
volatile解决有序性
volatile修饰的变量,在编译时,通过插入内存屏障来禁止指令重排序。
内存屏障:volatile修饰的变量,赋值后多执行了一个load add操作,这个操作相当于一个内存屏障

内存屏障的作用及原理

它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
它会强制将对缓存的修改操作立即写入主存;
如果是写操作,它会导致其他CPU中对应的缓存行无效。
原理:底层是通过CPU级别的lock来实现的,反编译volatile会发现多了一个lock前缀,他的作用就是在总线上加锁,其他CPU不能访问,其他CPU基于缓存一致性协议会将自己的缓存修改为无效,当其他CPU需要读取时会从内存中读取最新的值。

volatile变量读写操作的规则

对volatile变量读或写时,其前面的操作已经全部执行且对后面操作可见;后面的操作且还没有进行
重排序时,不能将volatile变量访问的语句放在后面执行,也不能把后面的语句放在前面执行。
也就是执行到volatile变量时,前面的所有的操作都必须执行完,且对volatile变量以及后面的语句可见。

happens-before原则

程序顺序:单线程顺序执行
监视器锁规则:对一个锁的解锁happens-before于随后对这个锁的加锁
volatile变量规则:volatile域的写happens-before任意后面对这个volatile域的读
传递性: A happens-before B, B happens-before C 则A happens- before C

volatile不保证原子性

i++等操作的特殊性

i++是先将主内存的i读取到工作内存,然后在工作内存中执行i+1 ,然后将执行加1操作后的值写入内存。这个操作不是原子操作,也就是会分为3部分来执行
两个线程同时执行i++操作的问题
假设有两个线程A ,B i=1
假设同时读取到i的值,这时线程A执行i+1的操作具体是创建了一个临时变量,即temp=i+1,注意这个操作并没有更改i的值,然后A线程执行i=temp,也就是把2这个值写入主内存,这时线程B读取的i会失效,只是i失效,之前存的temp仍然是2,然后B线程把temp的值写入内存,也就是i=2,最终少执行了一次i+1的操作,导致i本应为3结果为2.
也就是对于i++这样的操作,即使可见性保证值会失效,但不保证操作的原子性。

解决方法:使用synchronized
使用Lock体系的ReentrantLock
使用java并发包的原子类

双重校验锁

new对象具体分为3步, 1.分配内存,2.初始化对象,3.将引用指向这个地址。
public class TestInstance{
private volatile static TestInstance instance;

public static TestInstance getInstance(){        
	if(instance == null){                       
		synchronized(TestInstance.class){        
			if(instance == null){                
				instance = new TestInstance();   
			}
		}
	}
	return instance;                             
}

}
}

使用volatile修饰是为了防止指令重排序,假如没有修饰,new 对象如果发生了重排序 ,即1 2 3的顺序重排序为1 3 2,假设A线程new 对象发生重排序然后执行到3这个步骤,这时,另一个线程B如果进来就会判断不为null 但此时对象还没有初始化,线程B就会拿到一个无效对象。
第一个if的作用是只有对象为空才进行加锁,降低性能消耗。不为空就直接返回对象的引用了。
第二个if的作用:防止二次创建实例。
假设线程A和线程B,线程A已经进入了第一个if判断,然后准备加锁来创建实例,但这时线程B也进来了然后把锁抢到了,导致线程A阻塞,然后线程B创建好了实例,然后释放锁。释放锁后线程A恢复运行然后占有了锁,(线程上下文会记录上一次执行时的代码所在位置),这时如果没有第二个if的话,线程A会又创建一个新的实例,导致单例模式不满足单例。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值