为什么volatile不具备原子性?
volatile和synchronized可以说都是JMM,即Java Memory Model,Java内存模型的具体实现,java内存模型的主要目标是定义程序中变量的访问规则。即在虚拟机中将变量存储到主内存或者将变量从主内存取出这样的底层细节。具体的可以看这篇文章 https://www.jianshu.com/p/15106e9c4bf3,具体讲解了Java内存模型的定义。
被volatile修饰的变量具有内存可见性,在底层通过内存屏障实现在多线程之间的可见性。什么叫「内存屏障」,Memory Barrier,是一种指令,有很多种类型,用来强制刷出CPU Cache,在使用volatile后,当写入被修饰的变量时,JMM会在写入成功之后发送一条Write-Barrier指令,并且在其他线程访问被修饰的变量之前,会执行Read-Barrier指令。
为什么volatile不具备原子性?
假如要让一个被volatile修饰的int类型的数据连续自增,那么主要分成三步:
-
- 读取主存中的变量到当前线程
- 增加变量的值
- 将本地的值写会到主存中
对应的jvm指令为:
其中第四步是使用内存屏障指令。具体作用是将本处理器的缓存写入到主存中,那么这个动作会导致别的处理器或者内核无效化其缓存,通过这种方式可以达到变量的修改立刻被其他处理器看到,因为其他处理器没有缓存了,那么只能从主存中拿,但这也仅仅只针对还没有读取变量的线程,对于已读取的线程,该操作没有效果。
可以发现,当有两个线程A,B对一个被volatile修饰的W=1进行操作时,线程A拷贝内存中的W值到本地之后,自增,这时候CPU时间片被使用完,挂起线程A之后执行线程B,线程B执行了两次自增并且最终将数据刷新到内存中,那么这时候W=3;CPU唤醒A线程,继续执行,此时A线程中的W=2,刷新到内存中,那么也就将线程B的更新覆盖了,最后得到的结果变成了W=2,丢失更新。
volatile只能保证最后将更新的数据及时的返回到共享内存中,而不像CAS那样,保存一个历史值,从而在更新的时候也就不能保证数据是否被修改。
但是synchronized不一样,通过锁的形式独占变量的内存空间,即使当前操作线程被CPU挂起,其他线程也不能访问变量的内存地址。
volatile和synchronized的区别:
- volatile只能修饰变量,synchronized可以修饰变量、类、方法。
- volatile本质是告诉执行线程当前操作的变量是不确定的,存在并发问题,不具备原子性,但是不会阻塞线程;synchronized则会阻塞其他线程来达到原子性、可见性。
- jvm对synchronized有优化,但是volatile则没有。
tip:如果只是要保证数据的可见性,那么可以使用「final」关键字修饰