关于java:Volatile只会用不知道原理这篇文章带你深究volatile

11次阅读

共计 2528 个字符,预计需要花费 7 分钟才能阅读完成。

据说微信搜寻《Java 鱼仔》会变更强哦!

本文收录于 JavaStarter,外面有我残缺的 Java 系列文章,学习或面试都能够看看哦

(一)概述

要理解并发编程,首先就须要理解并发编程的三大个性:可见性、原子性和有序性。
咱们明天要讲的 volatile 保障了 可见性和有序性 ,然而 不保障原子性。接下来会通过几段代码和几张图来强化对 volatile 的理解。

(二)volatile 保障可见性

在讲 JMM 的时候,咱们写了一段程序,并晓得了两个不同线程之间操作数据是不可见的,即线程 B 批改了主内存中的共享变量后,线程 A 是不晓得的。这就是线程之间的不可见性。

public class Test {
    private static boolean flag=false;
    public static void main(String[] args) throws InterruptedException {new Thread(new Runnable() {
            @Override
            public void run() {System.out.println("waiting");
                while (!flag){}
                System.out.println("in");
            }
        }).start();

        Thread.sleep(2000);
        new Thread(new Runnable() {
            @Override
            public void run() {System.out.println("change flag");
                flag=true;
                System.out.println("change success");
            }
        }).start();}
}

这段代码的后果是第二个线程批改 flag 的值不会被第一个线程见到

当初咱们做个小小的扭转,给 flag 加上 volatile 修饰词

private static volatile boolean flag=false;

对 volatile 原理的了解还是须要借助 JMM,咱们拿上来第一段代码的执行流程图:

这是未加 volatile 时的执行过程,最初会停留在第十步,flag 会变成 true,然而线程 A 不晓得。

加上 volatile 后,当主内存的 flag 被扭转时,volatile 通过 cpu 的总线嗅探机制,将其余也正在应用该变量的线程的数据生效掉,使得这些线程要从新读取主内存中的值,最初线程 A 就发现 flag 的值被扭转了。

(三)Volatile 保障有序性

Volatile 通过内存屏障禁止指令的重排序,从而保障执行的有序性。具体的内容我在指令重排序和内存屏障的时候讲到了,有趣味的小伙伴能够看一下。

(四)Volatile 不保障原子性

首先还是拿出一段代码,这段代码很简略,定义一个 count,并且用 volatile 润饰。接着创立十个线程,每个线程循环 1000 次 count++:

public class VolatileAtomSample {
    private static volatile int count=0;
    public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread(new Runnable() {
                @Override
                public void run() {for (int j = 0; j < 1000; j++) {count++;}
                }
            }).start();}

        try {Thread.sleep(1000);
        } catch (InterruptedException e) {e.printStackTrace();
        }
        System.out.println(count);
    }
}

最初无论执行多少次,你会发现最初输入的 count 绝大多数都是小于 10000,景象曾经体现出了 volatile 不能保障原子性,然而为什么呢?


还是通过一个流程图来示意,当线程 A 执行完 count+ 1 后,将值写回到主内存,这个时候因为 volatile 的可见性,其余工作内存中的 count 值会被生效掉从新赋值。

可如果线程 B 刚好执行到第四步呢,线程 B 工作内存中的 count 因为 volatile 变成了 1,assign 赋值后的 count 还是等于 1 ,在这里间接少了一次 count++。这就是 volatile 不能保障原子性的起因。

(五)Volatile 的应用场景

通过一个很经典的场景来展现一下 volatile 的利用,双重校验单例:

public class Singleton {
    private static Singleton Instance;
    private Singleton(){};
    public static Singleton getInstance(){if (Instance==null){synchronized (Singleton.class){Instance=new Singleton();
            }
        }
        return Instance;
    }
}

下面这段代码置信大家必定很相熟,单例模式最经典的一段代码,获取实例对象时如果为空就初始化,如果不为空就返回实例,看着没有问题,然而在高并发环境下这段代码是会出问题的。
Instance=new Singleton(); 实例化一个对象时,须要经验三步:

留神,这三步是有可能产生指令重排序的 ,因而有可能是先申请内存空间,再把对象赋值到内存里,最初实例化对象。 第一步 -> 第三步 -> 第二步 的形式来执行。

当此时有两个线程 A 和 B 同时申请对象的时候,当线程 A 执行到重排序后的第二步时


线程 B 执行了 if (Instance==null)这行代码,因为此时 instance 曾经赋值到内存里了,所以会间接 return Instance; 然而!这个对象并没有被实例化,因而线程 B 调用该实例时,就报错了。

这个问题的解决办法就是 volatile 禁止重排序

private static volatile Singleton Instance;

(六)Volatile 可能会导致的问题

凡事都考究一个度,volatile 也是。如果一个程序中用了大量的 volatile,就有可能会导致总线风暴,所谓 总线风暴,就是指当 volatile 润饰的变量产生扭转时,总线嗅探机制机会将其余内存中的这个变量生效掉,如果 volatile 很多,有效交互会导致总线带宽达到峰值。因而对 volatile 的应用也须要适度。

正文完
 0