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指令重排优化引起