浅谈Java中的volatile关键字

原创 2018年04月15日 20:54:30

(1)内存可见性:

        volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演者重要角色。同Synchronized相比(Synchronized称为重量级锁),volatile更轻量级,能保证多个线程共享资源的可见性。

public class TestVolatile {
    boolean status = false;

    /**
     * 状态切换为true
     */
    public void changeStatus(){
        status = true;
    }

    /**
     * 若状态为true,则running。
     */
    public void run(){
        if(status){
            System.out.println("running....");
        }
    }
}

上面这个例子,在多线程环境里,假设线程A执行changeStatus()方法后,线程B运行run()方法,可以保证输出"running....."吗?

  答案是NO! 

  这个结论会让人有些疑惑,可以理解。因为倘若在单线程模型里,先运行changeStatus方法,再执行run方法,自然是可以正确输出"running...."的;但是在多线程模型中,是没法做这种保证的。因为对于共享变量status来说,线程A的修改,对于线程B来讲,是"不可见"的。也就是说,线程B此时可能无法观测到status已被修改为true。那么什么是可见性呢?

        所谓可见性,是指当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。很显然,上述例子中是没有办法做到内存的可见性。

        Java的内存模型:

        为什么出现这种情况呢?我们先了解一些JMM(Java的内存模型)

        Java虚拟机有自己的内存模型(Java Memory Model),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

        JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main  Memory)中,每个线程都有一个私有的本地内存(Local  Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程变量的所有操作都必须在工作内存中进行,而不能直接写主内存中的变量。

            这三者之间的交互关系如下:

                                    

        需要注意的是,JMM是个抽象的内存模型,所以所谓的本地内存、主内存都是抽象概念,并一定就真实的对应cpu缓存和物理内存。

       大概了解了JMM的简单定义后,问题就很容易理解了,对于普通的共享变量来讲,比如我们上文中的status,线程A将其修改为true这个动作发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B缓存了status的初始值false,此时可能没有观测到status的值被修改了,所以就导致了上述的问题。那么这种共享变量在多线程模型中的不可见性如何解决呢?比较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,有点炮打蚊子的意思。比较合理的方式其实就是volatile!

       volatile具备两种特性,第一就是保证共享变量对所有线程的可见性。将一个共享变量声明为volatile后,会有以下效应:

            (1) 当写一个volatile变量时,JMM会把该线程对应的本地内存中的数据强制刷新到主内存中去;

            (2) 这个写操作会导致其他线程中的缓存无效,使用时再次读取主内存中的最新数据进行操作。

上面的例子只需将status声明为volatile,即可保证在线程A将其修改为true时,线程B可以立刻得知。

volatile boolean status = false;

(2)volatile不保证原子性操作

        对于volatile关键字,在一些多线程的单操作中可以保证线程安全,此时在同Synchronized相比,比较轻量级,适用性好。volatile并不能完全替代synchronized,它依然是个轻量级锁,在很多场景下,volatile关键字不能胜任。看例子:

public class Counter {
    public static volatile int num = 0;
    //使用CountDownLatch来等待计算线程执行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);
    public static void main(String []args) throws InterruptedException {
        //开启30个线程进行累加操作
        for(int i=0;i<30;i++){
            new Thread(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        num++;//自加操作
                    }
                    countDownLatch.countDown();
                }
            }.start();
        }
        //等待计算线程执行完
        countDownLatch.await();
        System.out.println(num);
    }

}

执行结果:256343

        针对这个示例,会觉得疑惑,如果使用volatile关键字修饰共享变量可以保证可见性,那么结果不应该是30000?

问题就出在num++这个操作上,因为num++是个复合操作,不是原子性操作。这个操作可以理解为三步:

            (1)读取        (2)加一       (3)赋值

        所以,在多线程环境下,有可能线程A将num读取到本地内存中,此时其他线程可能已经将num增大了很多,线程A依然对过期的num进行自加,重新写到内存中,最终导致num的结果不合预期,而是小于30000。

(3)解决num++这类复合类的操作,可以使用Java并发包中的原子操作类时通过循环CAS的方式来保证原子性的。

    
public class Counter {
  //使用原子操作类
    public static AtomicInteger num = new AtomicInteger(0);
    //使用CountDownLatch来等待计算线程执行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);
    public static void main(String []args) throws InterruptedException {
        //开启30个线程进行累加操作
        for(int i=0;i<30;i++){
            new Thread(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        num.incrementAndGet();//原子性的num++,通过循环CAS方式
                    }
                    countDownLatch.countDown();
                }
            }.start();
        }
        //等待计算线程执行完
        countDownLatch.await();
        System.out.println(num);
    }
}

执行结果:30000

(4)禁止指令重排序

         volatile还有一个重大特性:禁止指令重排序

        重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。但是重排序也需要遵守一定的规则:

        (1) 重排序操作不会对存在数据依赖关系的操作进行重排序;

                比如:a = 1; b = a;这个指令,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序;

        (2) 重排序时是了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。

          

比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

  重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,问题就出来了,来开个例子,我们对第一个TestVolatile的例子稍稍改进,再增加个共享变量a

        
public class TestVolatile {
    int a = 1;
    boolean status = false;

    /**
     * 状态切换为true
     */
    public void changeStatus(){
        a = 2;//1
        status = true;//2
    }

    /**
     * 若状态为true,则running。
     */
    public void run(){
        if(status){//3
            int b = a+1;//4
            System.out.println(b);
        }
    }
}

假设线程A执行changeStatus后,线程B执行run,我们能保证在4处,b一定等于3么?

   答案依然是无法保证!也有可能b仍然为2。上面我们提到过,为了提供程序并行度,编译器和处理器可能会对指令进行重排序,而上例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。

   使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序

(5)总结:

           volatile是一种轻量级的同步机制,它主要有两个图特性:一是保证共享变量对所有线程的可见性;二是禁止指令重排序优化。同时需要注意的是,volatile对于单个的共享变量的读/写操作具有原子性,但是像num++这种复合操作,volatile就无法保证原子性。此时可以使用并发包中的原子操作类,通过循环CAS方式来保证num++操作的原子性,进而达到多个线程访问数据的安全性问题



版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/super_YC/article/details/79952326

java中的volatile关键字的功能详解

Cookie的应用场景: 1,记录用户的登陆状态,提示用户是否记住密码; 2,购物车购物功能; 我们知道,在web开发过程中,我们都要和cookie打交道,有时候离开了cookie还真玩不转。co...
  • reggergdsg
  • reggergdsg
  • 2015-12-11 22:01:24
  • 2577

java volatile关键字(及使用场景)

当一个变量被定义为volatile之后,就可以保证此变量对所有线程的可见性,即当一个线程修改了此变量的值的时候,变量新的值对于其他线程来说是可以立即得知的。可以理解成:对volatile变量所有的写操...
  • x_i_y_u_e
  • x_i_y_u_e
  • 2016-02-24 10:55:24
  • 2626

Java 关键字volatile 与 synchronized 作用与区别

1,volatile    volatile告诉jvm, 它所修饰的变量不保留拷贝,直接访问主内存中的。   在Java内存模型中,有main memory,每个线程也有自己的memory (例如...
  • HGuang_ZJH
  • HGuang_ZJH
  • 2014-04-13 23:10:42
  • 4258

java多线程(四)关键字volatile

关键字volatile的主要作用是使变量在多线程间可见。       解决异步死循环       如下代码       public class RunThread extends Thread...
  • lxy344x
  • lxy344x
  • 2016-06-22 21:18:06
  • 684

浅谈volatile关键字

今天学习CurrentHashMap源码,发现CurrentHashMap源码中有的属性是用volatile修饰的,别人说volatile可以保证读写的内容是最新的。看到这句话的时候还是满脑子的问号。...
  • wangbiao007
  • wangbiao007
  • 2016-10-09 12:05:28
  • 721

Java关键字volatile,原子性,变量可见性

内存模型与CPU缓存 本来CPU计算的数字都是从主从main memory中读取的,但是CPU运行的速度比计算机读取内存的速度快,为了补齐这个短板,所以出现了CPU缓从这种东西。 在多CPU...
  • Matthewhou
  • Matthewhou
  • 2017-07-20 14:49:56
  • 419

Java关键字系列 — volatile、synchronized、lock

volatile、synchronized和lock简单介绍
  • u011760575
  • u011760575
  • 2017-01-04 00:01:57
  • 523

Java线程面试题(03) Java中的volatile如何工作? Java中的volatile关键字示例

什么是Java中的volatile变量以及何时使用Java中的volatile变量是一道Java面试中经常被问及的多线程访问问题。尽管许多程序员知道什么是volatile变量,但是他们在回答第二部分问...
  • u010096900
  • u010096900
  • 2017-11-27 13:03:50
  • 698

面试题--java中volatile关键字的含义

本文转载自:http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html在java线程并发处理中,有一个关键字volatile的使用目...
  • Mrzhoug
  • Mrzhoug
  • 2016-08-20 14:58:36
  • 652

Java线程安全之volatile关键字

我们知道在多线程的场景下,线程安全是必须要着重考虑的。Java语言包含两种内在的同步机制:同步块(synchronize关键字)和 volatile 变量。但是其中 Volatile 变量虽然使用简单...
  • Roy_70
  • Roy_70
  • 2017-04-07 10:37:02
  • 3748
收藏助手
不良信息举报
您举报文章:浅谈Java中的volatile关键字
举报原因:
原因补充:

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