【并发】Volatile作用详解

  • volatile
    • 保证变量的可见性
    • 禁止指令重排
    • 不保证原子性
      • 如何保证原子性

volatile

  • volatile关键字可以保证变量的可见性。

  • 被volatile修饰的变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。

  • 无法保证原子性

保证变量的可见性

当多个线程访问同一个变量的时候,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。

如果不能保证可见性的话,会导致其他线程无法及时看到修改的值,可能会导致死循环等问题的出现。

使用volatile可以保证可见性,及时通知其他线程。当一个共享变量被volatile修饰时,会保证修改的值立即更新到主存,当有其他线程需要读取时,会从内存中读取新值。这是关键的地方。

禁止指令重排

指令重排序是指编译器和运行时对指令作出重新排序的行为。

禁止指令重排序的关键是执行内存屏障。

public native void loadFence();   // 添加读屏障
public native void storeFence();  // 添加写屏障
public native void fullFence();   // 读写屏障

【并发】Volatile作用详解_第1张图片

不保证原子性

由于volatile的可见性,可能会导致原子性无法保证。这又是为什么呢?

看下面这段代码:

public class TestDemo2 {
    public static class MyTest {
        public volatile int num = 0;
        public void numPlusPlus() {
            num++;
        }
    }

    public static void main(String[] args) {
        MyTest myTest = new MyTest();
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 2000; j++) {
                    myTest.numPlusPlus();
                }
            }, "Thread" + i).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        
        //结果应该是20000
        System.out.println(Thread.currentThread().getName() + "\t finally num value is " + myTest.num);
    }
}

如果volatile保证原子性的话,结果就应该是20000,但是我们可以看到三次运行的结果是不同的。

【并发】Volatile作用详解_第2张图片

注意:

num++;这一指令其实要执行三个操作:先取值,再自加(tmp),再赋值操作

当多个线程执行numPlusPlus()方法时,线程A读取num=0,执行num+1操作,此时num还没有变化;B线程执行num+1操作。此时A,B保存的num的值都是0,num+1(tmp)的值都为1。

A线程执行赋值的操作,此时,num=1,并将num的值刷新到内存中并通知其他线程保存的num值失效,B读取到num的值为1,而之前tmp保存的结果也为1。

B线程也执行赋值操作num = tmp,结果为1,比预期结果2少了1。

说白了,就是因为volatile不会加锁,多个线程能够同时操作同一变量。

如何保证原子性

使用JUC内部的AtomicInteger类来保证了我们变量相关的原子性。

你可能感兴趣的:(并发,java,jvm,开发语言)