博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java并发编程之volatile关键字解析
阅读量:6701 次
发布时间:2019-06-25

本文共 3581 字,大约阅读时间需要 11 分钟。

引言

volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。本文我们就从JVM内存模型开始,了解一下volatile的应用场景。

JVM内存模型

在了解volatile之前,我们有必要对JVM的内存模型有一个基本的了解。Java的内存模型规定了所有的变量都存储在主内存中(即物理硬件的内存),每条线程还具有自己的工作内存(工作内存可能位于处理器的高速缓存之中),线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量)。不同的线程之间无法直接访问对方工作内存之间的变量,线程间变量值的传递需要通过主内存来完成。

JVM内存模型

p.s: 对于上面提到的副本拷贝,比如假设线程中访问一个10MB的对象,并不会把这10MB的内存复制一份拷贝出来,实际上这个对象的引用,对象中某个在线程访问到的字段是有可能存在拷贝的,但不会有虚拟机实现把整个对象拷贝一次。

在并发编程中,我们通常会遇到以下三个问题:原子性,可见性,有序性,下面我们我们来具体看一下这三个特性与volatile之间的联系:

有序性

public class TestCase {    public static int number;    public static boolean isinited;    public static void main(String[] args) {        new Thread(                () -> {                    while (!isinited) {                        Thread.yield();                    }                    System.out.println(number);                }        ).start();        number = 20;        isinited = true;    }}

对于上面的代码我们上面的本意是想输出20,但是如果运行的话可以发现输出的值可能会是0。这是因为有时候为了提供程序的效率,JVM会做进行及时编译,也就是可能会对指令进行重排序,将isInited = true;放在number = 20;之前执行,在单线程下面这样做没有任何问题,但是在多线程下则会出现重排序问题。如果我们将number声名为volatile就可以很好的解决这个问题,这可以禁止JVM进行指令重排序,也就意味着number = 20;一定会在isInited = true前面执行。

可见性

比如对于变量a,当线程一要修改变量a的值,首先需要将a的值从主存复制过来,再将a的值加一,再将a的值复制回主存。在单线程下面,这样的操作没有任何的问题,但是在多线程下面,比如还有一个线程二,在线程一修改a的值的时候,也从主存将a的值复制过来进行加一,随后线程一和线程二先后将a的值复制回主存,但是主存中a的值最终将只会加一而不是加二。

使用volatile可以解决这个问题,它可以保证在线程一修改a的值之后立即将修改值同步到主存中,这样线程二拿到的a的值就是线程一已经修改过的a的值了。对volatile变量执行写操作时,会在写操作后加入一条store屏障指令,对volatile变量执行读操作时,会在写操作后加入一条load屏障指令。

线程写volatile变量过程:

  1. 改变线程工作内存中volatile变量副本的值;

  2. 将改变后的副本的值从工作内存刷新到主内存。

线程读volatile变量过程:

  1. 从主内存中读取volatile变量的最新值到工作内存中;

  2. 从工作内存中读取volatile变量副本。

原子性

原子性是指CPU在执行一条语句的时候,不会中途转去执行另外的语句。比如i = 1就是一个原子操作,但是++i就不是一个原子操作了,因为它要求首先读取i的值,然后修改i的值,最后将值写入主存中。

但是volatile却不能保证程序的原子性,下面我们通过一个实例来验证一下:

public class TestCase {    public volatile int v = 0;    public static final int threadCount = 20;    public void increase() {        v++;    }    public static void main(String[] args) {        TestCase testCase = new TestCase();        for (int i=0; i
{ for (int j=0; j<1000; j++) { testCase.increase(); } } ).start(); } while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(testCase.v); }}

输出结果:

18921

上面我们的本意是想让输出20000,但是运行程序后,结果可能会小于20000。因为v++它本身并不是一个原子操作,它是分为多个步骤的,而且volatile本身也并不能保证原子性。

上面的程序使用synchronzied则可以很好的解决,只需要声明public synchronized void increase()就行了。

或者使用lock也行:

Lock lock = new ReentrantLock();public void increase() {    lock.lock();    try {        v++;    } finally {        lock.unlock();    }}

或者将v声明为AtomicInteger v = new AtomicInteger();。在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的自增,自减,以及加法操作,减法操作进行了封装,保证这些操作是原子性操作。

单例模式

下面我们通过单例模式来看一下volatile的一个具体应用:

class Singleton {    private volatile static Singleton instance;    public static Singleton getInstance() {        if (instance == null) {            synchronized (Singleton.class) {                if (instance == null)                    instance = new Singleton();            }        }        return instance;    }    public static void main(String[] args) {        Singleton.getInstance();    }}

上面instance必须要用volatile修饰,因为new Singleton是分为三个步骤的:

  1. 给instance指向的对象分配内存,并设置初始值为null(根据JVM类加载机制的原理,对于静态变量这一步应该在new Singleton之前就已经完成了)。

  2. 执行构造函数真正初始化instance

  3. 将instance指向对象分配内存空间(分配内存空间之后instance就是非null了)

在我们的步骤2, 3之间的顺序是可以颠倒的,如果线程一在执行步骤3之后并没有执行步骤2,但是被线程二抢占了,线程二得到的instance是非null,但是instance却还没有初始化。而使用volatile则可以保证程序的有序性。

References

Contact

GitHub:

Blog:

本文为作者原创,转载请声明:)

你可能感兴趣的文章
VMware嵌套虚拟化
查看>>
CPP 调用简单的汇编函数
查看>>
Golang Slice interface and array concatenation
查看>>
java nio
查看>>
nginx反向代理配置及优化
查看>>
linux下使用free命令查看实际内存占用
查看>>
整合 Tachyon 运行 Spark(译)
查看>>
外部链接增加nofllow
查看>>
mysql update safe model 一些问题
查看>>
Java项目开发环境构建工具 Gradle 使用笔记(简单、基本)
查看>>
Scala的模式匹配和条件类
查看>>
Scala函数与对象
查看>>
前端Jquery小结
查看>>
浅谈Java的输入输出流
查看>>
Eclipse安装SVN插件
查看>>
gradle3.1初步了解
查看>>
网易招聘笔试题+答案解析
查看>>
atoi和itoa函数的实现
查看>>
Puppet 2.7 SSH安装配置-3
查看>>
linux普通用户利用authbind绑定特权端口
查看>>