java多线程(并发)夯实之路-volatile深入浅出

volatile

volatile(易变关键字)可以用来修饰成员变量和静态成员变量,线程只能从主存中获取它的值,线程操作volatile变量都是直接操作主存

与synchronzied区别:synchronzied需要创建Monitor,属于重量级的操作,volatile更轻量(推荐)

可见性

一个线程对主存的数据进行了修改,另一个线程不可见

如下,t线程并不会停下来,因为main线程修改了run值,并同步至主存,但t线程是从自己工作内存中的高速缓存中读取run值,缓存中的run值没变,仍是true(t线程要频繁地访问run的值,JIT编译器会将run值存入高速缓存中,提高效率)

解决方法:给这个变量加volatile修饰,或者用synchronzied         

可见性VS原子性

可见性是修改变量对所有线程可见,线程能得到正确的值,不能保证原子性

原子性是保证一整块操作是不可分割的,否则上下文切换发生线程安全问题 volatile适用一个线程修改变量,多个线程读取变量的情况

synchronzied语句块可以保证代码块的原子性,也可以保证代码块内变量的可见性,缺点是属于重量级操作,性能相对更低

有序性

JVM会在不影响正确性的前提下,调整语句的执行顺序

这一特性被称为指令重排,多线程下指令重排会影响正确性

指令重排原理:每个指令分为五个个阶段,不改变结果的前提下,指令的各个阶段可以通过重排序和组合来实现指令级并行(分阶段,分工是提升效率的关键)

就像

现代CPU支持多级指令流水线,可以同时执行取指令-指令译码-执行指令-内存访问-数据写回的处理器就可以称为五级指令流水线,CPU可以在一个时钟周期内,同时运行五条指令的不同阶段,IPC=1,流水线技术并没有缩短单条指令的执行时间,但变相地提高了指令的吞吐率

指令重排产生的问题:

如以下代码,可能发生指令重排如下右图所示,r值可能为0(很难出现)

解决方案:给ready变量加上volatile

操作volatile变量之前的代码不会重排序

volatile原理

volatile的底层实现是内存屏障,Memory Barrier(Memory Fence)对volatile变量的写指令后会加入写屏障

对volatile变量的读指令前会加入读屏障

保证可见性:

写屏障(sfence)保证该屏障前对共享变量的写入,都同步到主存中

读屏障(lfence)保证该屏障后对共享变量的读取,都从主存中加载

即保证了volatile变量的写操作及之前的写操作,volatile变量读操作及之后的读操作都是在主存中进行的,是准确的。

保证有序性:

写屏障保证指令重排时,写屏障之前的代码不会排在写屏障之后读屏障保证指令重排时,读屏障之后的代码不会排在读屏障之前即指令重排时,写屏障之前和读屏障之后的代码不会跨越屏障

但是并不能解决指令交错(上下文切换),只能保证一个线程中相关变量读写准确,以及一个线程中相关代码不被重排序

double-checked locking问题


以著名的double-checked locking单例模式为例

以上代码的实现特点是:

·懒惰实例化

·首次使用getInstance()才用synchronized加锁,之后不用加锁

·但很关键的一点:第一个if使用INSTANCE变量,在同步块之外

在多线程环境下,代码是有问题的

也许jvm会优化为:INSTANCE = new SIngleton()先赋值再调用SIngleton构造方法。

而在调用SIngleton构造方法前,INSTANCE不为空,可能另一个线程已经去使用INSTANCE对象

synchronzied代码中仍然有指令重排,但如果变量完全被synchronzied保护,就不会有原子性,可见性以及有序性问题

解决方案:给INSTANCE变量加上volatile

happens-before

happens-before规定对共享变量的写操作对其它线程的读操作,它是有序性和可见性的一套规则总结。

抛开以下happens-before规则,JVM并不能保证一个线程对共享变量的写,对于其它线程的读可见:

线程解锁之前对变量的写,对之后其它线程加锁对变量的读是可见的

线程对volatile变量的写,对之后其它线程对该变量的读可见

线程start前对变量的写,对之后线程开始对变量的读可见

线程结束前对变量的写,对其它线程得知它结束后的读可见(如调用t1.join()或 t1.isAlive())

线程被打断前对变量的写,对其它线程得知它被打断后的读可见(通过t1.interrupt()或 t1.isInterruputed())

对变量默认值(0,flase,null)的写,对其它线程对该变量的读可见

具有传递性(如果x hb-> y 并且y hb-> z 那么x hb-> z,配合volatile防止指令重排,有下面的例子)

静态成员变量初始化操作在类加载阶段实现的,类加载阶段由JVM保证代码的线程安全性

可见性问题由JVM缓存优化引起,有序性问题由JVM指令重排优化引起

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值