天天看点

java volatile关键字总结

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

目录

      • 目录
      • java关键字volatile总结
        • 线程的可见性
        • 指令重排序

java关键字volatile总结

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

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

线程的可见性

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

java volatile关键字总结

由上述可以得出,多个线程对主存中同一普通变量的修改,是存在”可见性”问题的,也就是指在一个线程中对变量修改后,其他线程不一定及时知道。而虚拟机会保证对于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 = ;

    private static CountDownLatch end = new CountDownLatch();

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

    public static void main(String[] args) {
        for(int i = ; i < ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        for(int i = ; i < ; 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+;
        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/

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