Java volatile变量原子性讨论

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   ; Load
inc    %r8d             ; Increment
mov    %r8d,0xc(%r10)   ; Store
lock   addl $0x0,(%rsp) ; StoreLoad Barrier

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值