原创

volatile关键字解析

引言

在上文《Java 中能创建 volatile 数组吗?》中,我们执行了一段代码,里面有一行 volatile int y = 0;, 我们会发现在并发1000操作进行 y++ 或者 y=y+1 时, 并不能保证一定得到 y=1000的值。为啥?

volatile关键字的两层语义

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。

volatile保证原子性吗?

volatile关键字保证了操作的可见性,但是volatile不保证原子性。

  • 保证单次读或单次写操作的原子性。
  • 不保证自增操作或多次读写操作的顺序性, 也就没办法保证原子性。

可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

保证可见性的原理: volatile修饰的变量发生修改时,volatile会强制刷新主内存,并使其他线程的工作内存失效,使其他线程必须从主内存重新读取,这样就保证了volatile 修饰变量的线程可见性。[反映到硬件层,就是CPU的L1或者L2缓存中对应的缓存行无效]

因此回到本文一开始的问题,y++ 或者 y=y+1 这类简单的自增操作是不具备原子性的。这类自增操作包括读取变量的原始值、进行加1操作、写入工作内存。也就是说自增操作的三个子操作在并发场景下可能会分割开执行。
而volatile操作不保证原子性,因此计算的数据结果存在极大的不确定性。

保证原子性的操作

  • synchronized注解修饰
  • 采用 Lock lock = new ReentrantLock()
  • 采用AtomicInteger。(atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。)

volatile的原理和实现机制

摘自《深入理解Java虚拟机》
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  • 1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  • 2)它会强制将对缓存的修改操作立即写入主存;
  • 3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

Synchronized和Volatile的比较

1)Synchronized保证内存可见性和操作的原子性
2)Volatile只能保证内存可见性
3)Volatile不需要加锁,比Synchronized更轻量级,并不会阻塞线程(volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。)
4)volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化(如编译器重排序的优化).
5)volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符。

volatile本质是在告诉JVM当前变量在寄存器中的值是不确定的,使用前,需要先从主存中读取,因此可以实现可见性。而对n=n+1,n++等操作时,volatile关键字将失效,不能起到像synchronized一样的线程同步(原子性)的效果。

正文到此结束
广告是为了更好的提供数据服务
本文目录