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关键字解析

  • 2016年11月27日 10:38
  • 680KB
  • 下载

Java线程安全之volatile关键字

我们知道在多线程的场景下,线程安全是必须要着重考虑的。Java语言包含两种内在的同步机制:同步块(synchronize关键字)和 volatile 变量。但是其中 Volatile 变量虽然使用简单...
  • Roy_70
  • Roy_70
  • 2017年04月07日 10:37
  • 2183

java并发编程 -volatile关键字

java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他的线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量...

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

--> html { line-height:...

Java线程:volatile关键字

Java线程:volatile关键字   Java™ 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。其中 Volat...

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

Java并发编程:volatile关键字解析    volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意...

Java中的volatile关键字

由于基础比较薄弱,在Java线程并发处理中,还是第一次看到volatile关键字,在查找了一些资料后做以下笔记,供查阅。...

Java关键字transient和volatile小结(转)

transient和volatile两个关键字一个用于对象序列化,一个用于线程同步,都是Java中比较高阶的话题,简单总结一下。 transient transient是类型修饰符,只能用...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:java volatile关键字总结
举报原因:
原因补充:

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