Java volatile变量原子性讨论

来源:互联网 发布:软件找不到怎么卸载 编辑:程序博客网 时间:2024/06/06 05:12

JDK官方文档是这样形容volatile的:

The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes. A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable.

意思就是说,如果一个变量加了volatile关键字,就会告诉编译器和JVM的内存模型:这个变量是对所有线程共享的、可见的,每次jvm都会读取最新写入的值并使其最新值在所有CPU可见。volatile似乎是有时候可以代替简单的锁,似乎加了volatile关键字就省掉了锁。但又说volatile不能保证原子性(java程序员很熟悉这句话:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性)。
我们都知道Java volatile关键字是为了保证共享变量在多线程间的可见性,即某个线程修改了共享变量的值,其他线程可以能够立即读到这个修改后的值。可见性的实现原理可以简单的概况如下:volatile变量进行写操作时,JIT编译器会在生成的汇编指令后加上一个lock前缀的额外指令,这个lock指令会使得处理器缓存立即回写到主内存,并使得其他处理器缓存的该缓存行无效。
前面讨论了volatile变量的可见性,那么现在说说它的原子性,先看看下面的代码:

package com.company;import java.util.concurrent.CountDownLatch;import java.util.concurrent.atomic.AtomicInteger;/** * Created by root on 2017/10/14. */public class VolatileTest {    private static volatile int anIntV = 0;    private static AtomicInteger anIntA = new AtomicInteger(0);    public static void main(String[] args) {        CountDownLatch latch = new CountDownLatch(4);        new Thread(() -> {            for (int i = 0; i < 200000; i++) {                anIntV++;                anIntA.getAndIncrement();            }            latch.countDown();        }).start();        new Thread(() -> {            for (int i = 0; i < 200000; i++) {                anIntV++;                anIntA.getAndIncrement();            }            latch.countDown();        }).start();        new Thread(() -> {            for (int i = 0; i < 200000; i++) {                anIntV++;                anIntA.getAndIncrement();            }            latch.countDown();        }).start();        new Thread(() -> {            for (int i = 0; i < 200000; i++) {                anIntV++;                anIntA.getAndIncrement();            }            latch.countDown();        }).start();        try {            latch.await();        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println("anIntV = " + anIntV);        System.out.println("anIntA = " + anIntA);    }}

这段代码的执行结果如下
这里写图片描述
不同于anIntA每次执行结果都是800000,而且这里的anIntV每次执行的结果都不一样。我们可以先下结论,volatile变量没有保证变量的原子性。
我们回头来看看volatile变量为何不保证原子性。让一个volatile的integer自增(i++),其实要分成3步:
1)读取volatile变量值到local;
2)增加变量的值;
3)把local的值写回,让其它的线程可见。
这3步的jvm指令为:

mov    0xc(%r10),%r8d   ; Loadinc    %r8d             ; Incrementmov    %r8d,0xc(%r10)   ; Storelock   addl $0x0,(%rsp) ; StoreLoad Barrier

注意最后一步是内存屏障。
如果变量是volatile 变量,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。这意味着如果你对一个volatile字段进行写操作,你必须知道:1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
再回来看前面的JVM指令:从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。

参考
为什么volatile不能保证原子性而Atomic可以?