什么是可见性?为什么会出现”不可见“
我们已经知道
counter.increment();
编译成字节码为
getfield #2
iconst_1
iadd
putfield #2
上一篇已经说过,这里的字节码的执行过程是在工作内存中,但是getField和putField这二条指令其实是跟主内存有交互的,这里还是以Counter类的increment方法为例。
-
getField指令会从主存中读取count的值,但是并不是每次都从主存中读,因为CPU高速cache的存在,我们count值有可能会从cache中读,导致读的并不是最新的
-
putField指令会将count新的值写入主内存,但是也不是立即生效,别的CPU的高速cache中的count不会立即更新,CPU会使用缓存一致性协议来做同步,这个对我们是透明的。
正是因为CPU高速cache的存在,在多核环境中会有可见性的问题。这里额外提一句 ,之所以有高速cache存在,是为提高运行效率,现代CPU的速度比我们的内存快很多,如果每次都锁总线写主存,会导致执行速度下降很多,这是不可以接受的,木桶理论我们都能理解。这里我也画了一张图,来帮助大家理解。
那有没有办法解决可见性带来的问题呢?当然是有的,对于Java,我们可以使用volatile关键字。
volatile
volatile修饰的变量有下面的特性
-
在写volatile的时候,有monitor release的语义,会刷新各个cpu中该变量的cache,存入最新的值
-
在读volatile的时候,有monitor acquire的语义,会使当前cpu的cache中该变量的cache失效,从主存中读取最新的值
-
volatile拥有禁止指令重排序的语义
其中monitor可以理解为锁,moniter release就是释放锁,monitor acquire就是获取锁,这样就是volatile变量的读写都是直接对主存操作的,相当于牺牲一部分性能来换取可见性,这一部分牺牲的性能一般是可以忽略不计的,只需要知道有这么回事就行。
volatile实现原理
给count加上volatile修饰符后,查看编译后的字节码后会发现,字节码层面唯一的变化是给count添加了ACC_VOLATILE标识flag,在运行时会根据这个flag会自动插入内存屏障,保证volatile可见性语义,内存屏障一共有四种,分别是:
-
LoadLoad
-
LoadStore
-
StoreStore
-
StoreLoad
这里给出文档中的一个实例,比较形象的说明了内存屏障是怎么插入的。
再回到上面的例子,我们给count添加上volat