上篇文章介绍了Java内存模型,没看过《深入理解Java虚拟机》的同学可以去看下Java内存模型
Java内存模型对volatile专门定义了一些特殊的访问规则,假定T表示一个线程,V和W分别表示两个volatile变量,那么在进行read、load、use、assign、store、和write操作时需要满足如下规则
1)只有当线程T对变量V执行的前一个动作时load的时候,线程T才能对变量V执行use动作。并且只有线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load动作。线程T对变量V的use动作可以认为是和线程T对变量V的load、read动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)
2)只有当线程T对变量V执行的前一个动作时assign的时候,线程T才能对变量V执行store动作,并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。线程T对变量V的assign动作可以认为是和线程T对变量V的store、wtite动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看见自己对变量V所做的修改)
3)假定动作A是线程T对变量V实施的use或assign动作,动作F是和动作A相关联的load或store动作,假定动作P是和动作F相对应的对变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修改的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)
可以说volatile关键字是Java虚拟机提供的最轻量级的同步机制。
当一个变量定义为volatile之后具备如下特性:
1.可见性:指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。普通变量的值在线程间传递均需通过主内存来完成,例:线程A修改了一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了后再从主内存进行读取操作,新变量的值才会对线程B可见。
虽然volatile型变量是线程可见的,对volatile变量所有的读写都能立刻反应到其他线程之中,但基于volatile变量的运算在并发下并不是安全的,因为Java里面的运算并不是原子操作。例:race ++,我们用javap反编译这句代码会发现在Class文件中是由4条字节码指令构成,(这里图1是书中原图,图2是笔者自己反编译的,可能由于Java虚拟机版本的问题2者不太一样,请知道的大佬告知,为文章严谨性看图1就可以了)
从字节码层面很容易分析出来,假如有两条线程同时执行这条语句,线程A执行getstatic指令把race的值取到操作栈顶时,volatile关键字保证来race的值在此时是正确的,但是执行iconst_1、iadd这些指令时线程B可能已经把race的值加大了,而在操作栈定的值就变成了过期的数据。
由于volatile变量只能保证可见性,所以在不符合以下两条规则的运算场景中,我们仍然要通过加锁来保证原子性
1)运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
2)变量不需要与其他的状态变量共同参与不变约束
2.禁止指令重排序优化,这里说明下,为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行优化。与之类似的Java虚拟机的即时编译器也有指令重排序优化。还不知道的同学可以去查下(说的就是我自己)。
普通的变量仅仅会保证在该方法的执行过程中所有依赖值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。这样理解起来还是有点抽象,笔者举个自己在书中看了茅塞顿开的例子
这里是一段汇编代码,方便起见就用书中原图了
这是段标准的DCL单例,变量用valatile修饰,赋值后(mov%eax,0x150(%esi)这句便是赋值操作)多执行了一个“lock addl & 0x0,(%esp)”操作,这个操作相当于一个内存屏障,只有一个cpu访问时并不需要内存屏障,但如果有两个或更多cpu访问同一块内存,且其中一个在观测另一个,就需要内存屏障来保证一致性了。这句指令“addl & 0x0,(%esp)”(把esp寄存器的值加0)显然是一个空操作,关键在于lock前缀,它的作用是使得本cpu的Cache写入了内存,该写入动作也会引起别的cpu或者别的内核无效化其Cache,这中操作箱单于对Cache中的变量做了一个“store和write”操作。所以通过这样一个空操作可以让前面valatile变量的修改对其他cpu立即可见。
这段基本是书中原文,这样说可能有的同学不太懂(我就没懂),下面在结合具体代码说下,instance = new Singleton() 这句代码并不是一个原子操作,大致做了3件事情
1)给Singleton实例分配内存空间
2)初始化Singleton对象
3)将instance对象指向分配的内存空间
由于Java虚拟机指令重排序优化,使得2、3的顺序是无法保证的,如果出现了132的情况,并且在3执行完毕2未执行之前线程切换了,这个时候instance非空,所以直接返回instance,结果可想而知
避免篇幅过长就到这吧