首先要了解的是,volatile可以保证可见性和顺序性,这些都很好理解,那么它为什么不能保证原子性呢?
可见性
可见性与Java的内存模型有关,模型采用缓存与主存的方式对变量进行操作,也就是说,每个线程都有自己的缓存空间,对变量的操作都是在缓存中进行的,之后再将修改后的值返回到主存中,这就带来了问题,有可能一个线程在将共享变量修改后,还没有来的及将缓存中的变量返回给主存中,另外一个线程就对共享变量进行修改,那么这个线程拿到的值是主存中未被修改的值,这就是可见性的问题。
volatile很好的保证了变量的可见性,变量经过volatile修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,这个不需要过多了解,但是加了这个指令后,会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使得在其他处理器缓存了该内存地址无效
什么意思呢?意思就是说当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值,这就保证了可见性。
原子性
问题来了,既然它可以保证修改的值立即能更新到主存,其他线程也会捕捉到被修改后的值,那么为什么不能保证原子性呢?
首先需要了解的是,Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。
所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。
举个栗子
首先线程A读取了i的变量的值,这个时候线程切换到了B,线程B同样从主内存中读取i的值,由于线程A没有对i做过任何修改,此时线程B获取到的i仍然是100。线程B工作内存中为i执行了加1的操作,但是没有刷新到主内存中,这个时候又切换到了A线程,A线程直接对工作内存中的100进行加1运输(因为A线程已经读取过i的值了),由于线程B并未写入i的最新值,这个时候A线程的工内存中的100不会失效。 最后,线程A将i=101写入主内存中,线程B也将i=101写入主内存中。 始终需要记住,i++ 的操作是3步骤!