java volatile关键字总结

原创 2015年07月07日 11:15:00

之前就看过很多关于volatile的资料,本文是作者对volatile关键字的一些总结,在这里先感谢《java内存模型》的作者程晓明。

目录

java关键字volatile总结

关于volatile修饰的变量,虚拟机做出如下保证:

  • 线程的可见性
  • 禁止指令的重排序

线程的可见性

java内存模型(简称JMM)规定了所有的变量都存储在主存中,每个线程都有自己的工作内存,工作内存中保存了主存中对应变量的拷贝,对变量的修改是在工作内存中完成,然后同步至主存中。JMM模型如图:
这里写图片描述

由上述可以得出,多个线程对主存中同一普通变量的修改,是存在”可见性”问题的,也就是指在一个线程中对变量修改后,其他线程不一定及时知道。而虚拟机会保证对于volatile的变量,修改是对其他线程立即可见的。那么虚拟机是如何做到这一点的呢?
在JMM中定义了八种操作来实现工作内存与主存的交互,这些操作都是原子操作,期间不会发生其他的线程切换:

  • Lock:将主存中的变量标记为一条线程独占状态;
  • Unlock:将锁定的变量释放;
  • Read:将主存中的变量传输到工作内存中;
  • Load:把read操作接收到的变量值放入工作内存的变量副本中;
  • Use:把工作内存中的值传递给执行引擎;
  • Assign:把从执行引擎中接收到的值赋值给工作内存中的变量;
  • Store:把工作内存中的变量传递至主存;
  • Write:将store接收到的变量的值赋值给主存中的变量;

在虚拟机中,对于volatile有如下规则,假设T表示一个线程,P和Q表示两个volatile变量,在进行上面描述的操作时:

  • 只有当T对P执行的前一个动作是load时,T才能对P执行use动作,并且只有T对P执行的后一个动作是use时,T才能对P进行load操作;这样就保证执行引擎每次在使用变量之前,都会从主存中读取最新的值。
  • 只有当T对P执行的前一个动作是assign时,T才能对P进行store操作,并且只有T对P执行的后一个动作是store时,T才能对P执行assign;这样就保证每次工作内存中的值修改后,会马上写入主存中。
  • 保证volatile的重排序规则(下文会有说明)

既然虚拟机对volatile变量做了这么多规定,这样可以保证volatile修饰的变量就是线程安全的吗?看例子:

package test;

import java.util.concurrent.CountDownLatch;

public class Test {

    public static volatile int num = 0;

    private static CountDownLatch end = new CountDownLatch(20);

    public static void addNum() {
        num++;
    }

    public static void main(String[] args) {
        for(int i = 0; i < 20; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        for(int i = 0; i < 10000; i++) {
                            addNum();
                        }
                    } finally {
                        end.countDown();
                    }
                }
            }).start();
        }

        try {
            end.await();
            System.out.println(num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

说明:20个线程,每个线程对num进行10000次自增操作,如果volatile是线程安全的,那执行完所有线程后应输出200000,但结果每次输出都不同,但都小于200000.
但是虚拟机不是规定对volatile变量的操作会对其他线程立即可见吗?怎么还会输出错误的结果呢?原因是:对num的操作 num++其实是一个复合操作而不是原子操作,也就是说,在执行num++时,会出现”可见性”问题。为了便于理解,可以参照synchronized关键字:

public class SynaTest {

    private volatile int num;//volatile变量

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public void add() {
        num++;
    }
}

等价于

public class SynaTest {

    private int num; //普通变量

    public synchronized int getNum() {
        return num;
    }

    public synchronized void setNum(int num) {
        this.num = num;
    }

    public void add() {
        int tmp = getNum();
        tmp = tmp+1;
        setNum(tmp);
    }
}

至此,关于第一点”对其他线程的可见”说完。

指令重排序

处理器和编译器为提高效率,可能会对程序进行指令重排序,但我们不会意识到这种操作,因为重排序不会影响程序的输出结果,当然,这里不影响输出结果只是在单线程中。那么JMM是如何是volatile修饰的变量不会发生指令重排序呢?

先来说说内存屏障,在JMM中,内存屏障可以分为:

屏障类型 指令示例 说明
LoadLoad Load1;LoadLoad;Load2 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载
StoreStore Store1;StoreStore;Store2 确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及后续存储指令的存储
LoadStore Load1;LoadStore;Store2 确保Load1数据装载,之前于Store2及后续的存储指令
StoreLoad Store1;StoreLoad;Load2 确保Store1数据对其他处理器变得可见(刷新到内存),之前于Load2及后续装载指令的装载。StoreLoad会使屏障之前的所有内存指令(存储和装载)完成之后,才执行该屏障之后的内存访问指令

在JMM中,关于volatile的重排序规则定义如下:

  • 当第二个操作是volatile写时,不论前一个操作是什么,都不能进行重排序。
  • 当第一个操作是volatile读时,不论后一个操作是什么,都不能进行重排序。
  • 第一个操作是volatile写,后一个操作是volatile读时,不能进行重排序

为了实现上述三点,JMM采用插入内存屏障:

  • 在每个volatile写操作的前面插入一个StoreStore屏障
  • 在每个volatile写操作的后面插入一个StoreLoad屏障
  • 在每个volatile读操作的后面插入一个LoadLoad屏障
  • 在每个volatile读操作的后面插入一个LoadStore屏障

通过这几个内存屏障,JMM就可以保证volatile语义:当写一个volatile变量时,JMM会把该线程对应的工作内存中的值刷新到主存中;档读一个volatile变量时,JMM会把工作内存中对应的变量值设为无效,从主存中获取变量值。

通过上述的描述,可以看出其实volatile并不是” 线程安全”的,如果要保证同步,还需要额外的同步手段,比如通过synchronized关键字或者java.util.concurrent工具,但是volatile在某些情况下是非常适用的,比如只有单一线程对volatile变量进行写操作:

public class VolaTest {

    volatile boolean stop = false;

    public void shutdown() {//调用该方法后,可以使所有线程的doWork立即停下来
        stop = true;
    }

    public void doWork() {
        while(!stop) {
            //...
        }
    }
}

参考:http://ifeve.com/java-memory-model-0/

如果有不对的地方,欢迎大家指正。

版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

Java——多线程总结、ThreadLocal/Volatile/synchronized/Atomic关键字

当线程被创建并启动之后,它既不是一启动就进入执行状态,也不是一直处于执行状态,在其生命周期中,要经过”新建(New)”、”就绪(Runnable)”、”运行(Running’)”、”阻塞(Blocke...

Java线程:volatile关键字

  • 2012-05-17 16:46
  • 30KB
  • 下载

Java内存模型与volatile关键字

Java内存模型(Java Memory Model)Java内存模型(JMM),不同于Java运行时数据区,JMM的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中读取...

Java并发编程:volatile关键字解析

Java并发编程:volatile关键字解析 Java并发编程:volatile关键字解析    volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之...

关于java中的synchronization与volatile关键字

java中的同步是用关键字synchronized和volatile实现 在java中,我们不能使用syncchronized去修饰变量,用synchronized去修饰变量是非法的。我们可以用vol...

java 关键字 volatile初识

java 关键字 volatile初识概述JMM提供了volatile变量定义、final、synchronized块来保证可见性。 用volatile修饰的变量,线程在每次使用变量的时候,都会读取变...

Java中volatile关键字

在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉。 Java语言是支持多线程的,为了解决线程并发的问题,在...

Java并发编程:volatile关键字解析

volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java 5之后,volatile关键字才得以...

Java并发编程:volatile关键字解析

volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java 5之后,volatile关键字才得以...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)