• Java Volatile关键字(转)


    出处:  Java Volatile关键字

      Java的volatile关键字用于标记一个变量“应当存储在主存”。更确切地说,每次读取volatile变量,都应该从主存读取,而不是从CPU缓存读取。每次写入一个volatile变量,应该写到主存中,而不是仅仅写到CPU缓存。

      实际上,从Java 5开始,volatile关键字除了保证volatile变量从主存读写外,还提供了更多的保障。我将在后面的章节中进行说明。

    变量可见性问题

      Java的volatile关键字能保证变量修改后,对各个线程是可见的。这个听起来有些抽象,下面就详细说明。

      在一个多线程的应用中,线程在操作非volatile变量时,出于性能考虑,每个线程可能会将变量从主存拷贝到CPU缓存中。如果你的计算机有多个CPU,每个线程可能会在不同的CPU中运行。这意味着,每个线程都有可能会把变量拷贝到各自CPU的缓存中,如下图所示:

      对于非volatile变量,JVM并不保证会从主存中读取数据到CPU缓存,或者将CPU缓存中的数据写到主存中。这会引起一些问题,在后面的章节中,我会来解释这些问题。

      试想一下,如果有两个以上的线程访问一个共享对象,这个共享对象包含一个counter变量,下面是代码示例:

    public class SharedObject {
        public int counter = 0;
    }

      如果只有线程1修改了(自增)counter变量,而线程1和线程2两个线程都会在某些时刻读取counter变量。

      如果counter变量没有声明成volatile,则counter的值不保证会从CPU缓存写回到主存中。也就是说,CPU缓存和主存中的counter变量值并不一致,如下图所示:

      这就是“可见性”问题,线程看不到变量最新的值,因为其他线程还没有将变量值从CPU缓存写回到主存。一个线程中的修改对另外的线程是不可见的。

    volatile可见性保证

      Java的volatile关键字就是设计用来解决变量可见性问题。将counter变量声明为volatile,则在写入counter变量时,也会同时将变量值写入到主存中。同样的,在读取counter变量值时,也会直接从主存中读取。

    下面的代码演示了如果将counter声明为volatile:

    public class SharedObject {
        public volatile int counter = 0;
    }

      将一个变量声明为volatile,可以保证变量写入时对其他线程的可见。

      在上面的场景中,一个线程(T1)修改了counter,另一个线程(T2)读取了counter(但没有修改它),将counter变量声明为volatile,就能保证写入counter变量后,对T2是可见的。

      然而,如果T1和T2都修改了counter的值,只是将counter声明为volatile还远远不够,后面会有更多的说明。

    完整的volatile可见性保证

      实际上,volatile的可见性保证并不是只对于volatile变量本身那么简单。可见性保证遵循以下规则:

      如果线程A写入一个volatile变量,线程B随后读取了同样的volatile变量,则线程A在写入volatile变量之前的所有可见的变量值,在线程B读取volatile变量后也同样是可见的。

      如果线程A读取一个volatile变量,那么线程A中所有可见的变量也会同样从主存重新读取。

    下面用一段代码来示例说明:

    public class MyClass {
        private int years;
        private int months
        private volatile int days;
    
        public void update(int years, int months, int days){
            this.years  = years;
            this.months = months;
            this.days   = days;
        }
    }

      update()方法写入3个变量,其中只有days变量是volatile。

      完整的volatile可见性保证意味着,在写入days变量时,线程中所有可见变量也会写入到主存。也就是说,写入days变量时,years和months也会同时被写入到主存。

    下面的代码读取了years、months、days变量:

    public class MyClass {
        private int years;
        private int months
        private volatile int days;
    
        public int totalDays() {
            int total = this.days;
            total += months * 30;
            total += years * 365;
            return total;
        }
    
        public void update(int years, int months, int days){
            this.years  = years;
            this.months = months;
            this.days   = days;
        }
    }

      请注意totalDays()方法开始读取days变量值到total变量。在读取days变量值时,months和years的值也会同时从主存读取。因此,按上面所示的顺序读取时,可以保证读取到days、months、years变量的最新值。

    ''译者注:可以将对volatile变量的读写理解为一个触发刷新的操作,写入volatile变量时,线程中的所有变量也都会触发写入主存。而读取volatile变量时,也同样会触发线程中所有变量从主存中重新读取。因此,应当尽量将volatile的写入操作放在最后,而将volatile的读取放在最前,这样就能连带将其他变量也进行刷新。上面的例子中,update()方法对days的赋值就是放在years、months之后,就是保证years、months也能将最新的值写入到主存,如果是放在两个变量之前,则days会写入主存,而years、months则不会。反过来,totalDays()方法则将days的读取放在最前面,就是为了能同时触发刷新years、months变量值,如果是放后面,则years、months就可能还是从CPU缓存中读取值,而不是从主存中获取最新值。''

    指令重排问题

    出于性能考虑,JVM和CPU是允许对程序中的指令进行重排的,只要保证(重排后的)指令语义一致即可。如下代码为例:

    int a = 1;
    int b = 2;
    
    a++;
    b++;

    这些指令可以按以下顺序重排,而不改变程序的语义:

    int a = 1;
    a++;
    
    int b = 2;
    b++;

    然而,指令重排面临的一个问题就是对volatile变量的处理。还是以前面提到的MyClass类来说明:

    public class MyClass {
        private int years;
        private int months
        private volatile int days;
    
        public void update(int years, int months, int days){
            this.years  = years;
            this.months = months;
            this.days   = days;
        }
    }

    一旦update()变量写了days值,则years、months的最新值也会写入到主存。但是,如果JVM重排了指令,比如按以下方式重排:

    public void update(int years, int months, int days){
        this.days   = days;
        this.months = months;
        this.years  = years;
    }

      在days被修改时,months、years的值也会写入到主存,但这时进行写入,months、years并不是新的值(译者注:即在months、years被赋新值之前,就触发了这两个变量值写入主存的操作,自然这两个变量在主存中的值就不是新值)。新的值自然对其他线程是不可见的。指令重排导致了程序语义的改变。

    Java对此有一个解决方法,我们会在下一章节中说明。

    Java volatile Happens-Before保证

      为了解决指令重排的问题,Java的volatile关键字在可见性之外,又提供了happends-before保证。happens-before原则如下:

      如果有读写操作发生在写入volatile变量之前,读写其他变量的指令不能重排到写入volatile变量之后。写入一个volatile变量之前的读写操作,对volatile变量是有happens-before保证的。注意,如果是写入volatile之后,有读写其他变量的操作,那么这些操作指令是有可能被重排到写入volatile操作指令之前的。但反之则不成立。即可以把位于写入volatile操作指令之后的其他指令移到写入volatile操作指令之前,而不能把位于写入volatile操作指令之前的其他指令移到写入volatile操作指令之后。

      如果有读写操作发生在读取volatile变量之后,读写其他变量的指令不能重排到读取volatile变量之前。注意,如果是读取volatile之前,有读取其他变量的操作,那么这些操作指令是有可能被重排到读取volatile操作指令之后的。但反之则不成立。即可以把位于读取volatile操作指令之前的指令移到读取volatile操作指令之后,而不能把位于读取volatile操作指令之后的指令移到读取volatile操作指令之前。

    以上的happens-before原则为volatile关键字的可见性提供了强制保证。

    ''译者注:这两个原则读起来有些拗口(当然翻译也不足够好),其实就是不管JVM怎么去禁止/允许某些情况下的指令重排,最终就是保证“完整的volatile可见性保证”的那种效果,所以,只要理解了“完整的volatile可见性保证”的效果就足够了 ''

    volatile并不总是可行的 

      虽然volatile关键字能保证volatile变量的所有读取都是直接从主存读取,所有写入都是直接写入到主存中,但在一些情形下,仅仅是将变量声明为volatile还是远远不够的。

      就像前面示例所说的,线程1写入共享变量counter的值,将counter声明为volatile已经足够保证线程2总是能获取到最新的值。

      事实上,多个线程都能写入共享的volatile变量,主存中也能存储正确的变量值,然而这有一个前提,变量新值的写入不能依赖于变量的旧值。换句话说,就是一个线程写入一个共享volatile变量值时,不需要先读取变量值,然后以此来计算出新的值。

      如果线程需要先读取一个volatile变量的值,以此来计算出一个新的值,那么volatile变量就不足够保证正确的可见性。(线程间)读写volatile变量的时间间隔很短,这将导致一个竞态条件,多个线程同时读取了volatile变量相同的值,然后以此计算出了新的值,这时各个线程往主存中写回值,则会互相覆盖。

      多个线程对counter变量进行自增操作就是这样的情形,下面的章节会详细说明这种情形。 

      设想一下,如果线程1将共享变量counter的值0读取到它的CPU缓存,然后自增为1,而还没有将新值写回到主存。线程2这时从主存中读取的counter值依然是0,依然放到它自身的CPU缓存中,然后同样将counter值自增为1,同样也还没有将新值写回到主存。如下图所示:

      从实际的情况来看,线程1和线程2现在就是不同步的。共享变量counter正确的值应该是2,但各个线程中CPU缓存的值都是1,而主存中的值依然是0。这是很混乱的。即使线程最终将共享变量counter的值写回到主存,那值也明显是错的。

    总结:

    volatile关键字可以达到的效果:

    当一个变量被定义为volatile之后,它对所有的线程就具有了可见性,也就是说当一个线程修改了该变量的值,所有的其它线程都可以立即知道,可以从两个方面来理解这句话:

      1.线程对变量进行修改之后,要立刻回写到主内存。

      2.线程对变量读取的时候,要从主内存中读,而不是工作内存。

    但是这并不意味着使用了volatile关键字的变量具有了线程安全性,举个栗子:

    public class AddThread implements Runnable {
        private volatile int num=0;
        @Override
        public void run() {
            for (int i=1;i<=10000;i++){
                num=num+1;
                System.out.println(num);
            }
        }
    }
    
    public class VolatileThread {
        public static void main(String[] args) {
            Thread[] th = new Thread[20];
            AddThread addTh = new AddThread();
            for(int i=1;i<=20;i++){
                th[i] = new Thread(addTh);
                th[i].start();
            }
        }
    }

    这里我们创建了20个线程,每个线程对num进行10000次累加。

    按理结果应该是打印1,2,3.。。。。。200000 。

    但是结果却是1,2,3…..x ,x小于200000.

    为什么会是这样的结果?

    我们仔细分析一下这行代码:num=num+1;

    虽然只有一行代码,但是被编译为字节码以后会对应四条指令:

     

     

    1.Getstatic将num的值从主内存取出到线程的工作内存

    2.Iconst_1 和 iadd 将num的值加一

    3.Putstatic将结果同步回主内存

    在第一步Getstatic将num的值从主内存取出到线程的工作内存因为num加了Volatile关键字,可以保证它的值是正确的,但是在执行第二步的时候其它的线程有可能已经将num的值加大了。在第三步就会将较小的值同步到内存,于是造成了我们看到的结果。

    既然如此,Volatile在什么场合下可以用到呢?

    一个变量,如果有多个线程只有一个线程会去修改这个变量,其它线程都只是读取该变量的值就可以使用Volatile关键字,为什么呢?一个线程修改了该变量,其它线程会立刻获取到修改后的值。

    因为Volatile的特性可以保证这些线程获取到的都是正确的值,而他们又不会去修改这个变量,不会造成该变量在各个线程中不一致的情况。当然这种场合也可以用synchronized关键字

    当运算结果并不依赖变量的当前值的时候该变量也可以使用Volatile关键字,上栗子:

    public class shutDownThread implements Runnable {
        volatile boolean shutDownRequested;
        public void shutDown(){
            shutDownRequested = true;
        }
        @Override
        public void run() {
            while (!shutDownRequested) {
                System.out.println("work!");
            }
        }
    }
    
    public class Demo01 {
        public static void main(String[] args) throws InterruptedException {
            Thread[] th = new Thread[10];
            shutDownThread t = new shutDownThread();
            for(int i=0;i<=9;i++){
                th[i] = new Thread(t);
                th[i].start();
            }
            Thread.sleep(2000);
            t.shutDown();
        }
    }
      当调用t.shutDown()方法将shutDownRequested的值设置为true以后,因为shutDownRequested 使用了volatile ,所有线程都获取了它的最新值true,while循环的条件“!shutDownRequested”不再成立,“ System.out.println("work!");”打印work的代码也就停止了执行。

    何时使用volatile

      正如我前面所说,如果两个线程同时读写一个共享变量,仅仅使用volatile关键字是不够的。你应该使用 synchronized 来保证读写变量是原子的。(一个线程)读写volatile变量时,不会阻塞(其他)线程进行读写。你必须在关键的地方使用synchronized关键字来解决这个问题。

      除了synchronized方法,你还可以使用java.util.concurrent包提供的许多原子数据类型来解决这个问题。比如,AtomicLong或AtomicReference,或是其他的类。

      如果只有一个线程对volatile进行读写,而其他线程只是读取变量,这时,对于只是读取变量的线程来说,volatile就已经可以保证读取到的是变量的最新值。如果没有把变量声明为volatile,这就无法保证。

      volatile关键字对32位和64位的变量都有效。

    volatile的性能考量

      读写volatile变量会导致变量从主存读写。从主存读写比从CPU缓存读写更加“昂贵”。访问一个volatile变量同样会禁止指令重排,而指令重排是一种提升性能的技术。因此,你应当只在需要保证变量可见性的情况下,才使用volatile变量。

  • 相关阅读:
    JS BOM对象 History对象 Location对象
    JS 字符串对象 数组对象 函数对象 函数作用域
    JS 引入方式 基本数据类型 运算符 控制语句 循环 异常
    Pycharm Html CSS JS 快捷方式创建元素
    CSS 内外边距 float positio属性
    CSS 颜色 字体 背景 文本 边框 列表 display属性
    【Android】RxJava的使用(三)转换——map、flatMap
    【Android】RxJava的使用(二)Action
    【Android】RxJava的使用(一)基本用法
    【Android】Retrofit 2.0 的使用
  • 原文地址:https://www.cnblogs.com/myseries/p/11939040.html
Copyright © 2020-2023  润新知