美文网首页
Java关键字Volatile

Java关键字Volatile

作者: high5130 | 来源:发表于2018-08-30 17:39 被阅读0次

    java关键字Volatile用于将java变量标记为存储在主内存中,这就意味着每次读取Volatile修饰的变量时都是从计算机的主内存中读取,而不是从CPU的缓存中读取,并且每次对Volatile变量写的时候都将写入主内存,而不仅仅是CPU缓存

    可见性问题

    Java关键字Volatile保证可以跨线程查看变量的变化,下面详细来说一下这个问题
    在线程操作非Volatile变量的多线程应用程序中,出于性能原因,每个线程可以在处理它们时将变量从主内存复制到CPU高速缓存中。如果您的计算机包含多个CPU,则每个线程可以在不同的CPU上运行。这意味着,每个线程可以将变量复制到不同CPU的CPU缓存中。这在这里说明:



    对于非Volatile变量,java虚拟机从主内存读取到CPU缓存,或从CPU缓存读取到主内存,这可能导致一系列问题:
    假设两个线程或者多个线程访问一个共享对象,共享对象包含一个counter变量,像这样:

    public class SharedObject {
        public int counter = 0;
    }
    

    假设只有线程1递增counter变量,但线程1和线程2都可能时不时读取counter变量。
    如果counter变量没有声明为Volatile,则无法保证counter从CPU缓存读取到主内存中的具体时间,这就意味着CPU缓存中的变量值可能跟主内存中变量值不同,这种情况如下所示:



    因为还没有被线程写入主线程,而导致另一个线程没有看到变量的最新值得问题,被称为线程的“可见性”问题
    ,线程的更新操作对其他线程不可见

    Java Volatile可见性保证

    java Volatile关键帧意在解决线程的可见性问题,通过对counter声明volatile,对象counter所有写的操作将立即写入主内存,同时对counter的读操作也是直接访问主内存

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

    因此声明一个Volatile,可以保证对其他线程的可见性
    在上面给出的场景中,一个线程(T1)修改计数器,另一个线程(T2)读取计数器(但从不修改它),声明了volatile的counter足以保证T2对counter变量写入的可见性。
    但是,如果T1和T2都在增加counter变量,那么 counter变量声明volatile就不够了。稍后会详细介绍。

    volatile完全可见性保证

    实际上,volatile的可见性保证超出了volatile变量本身,可见性保证如下:
    1.如果线程A 对volatile变量进行写操作,那么线程B可以立刻读取相同的volatile变量,在对volatile变量写前,所有的变量对线程A都是可见的。在读取volatile变量后对线程B同样是可见的。
    2.如果线程A读取volatile变量,则读取变量时线程A的所有可见volatile变量也将从主内存重新读取
    用实例代码来说明:

    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用了volatile
    volatile完全可见性意味着,当对days进行写入时,线程所有的可见变量都会写入主内存(不仅仅是volatile变量自己写入到主存中,其他被该线程修改的所有变量也会刷新到主存),这就意味着,当对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时,years和months也会读取到主内存中,因此可以保证读取到days、years和months的最新值

    指令重排

    只要指令的语义含义不变,jvm和CPU就可以出于性能的原因,重新排序指令的程序,看以下说明:

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

    这些指令可以按一下方式重排,而不会丢失程序的语义含义:

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

    然而,当程序中有一个volatile字段时,指令重新排序提出了挑战。让我们从MyClass类中看看一下java volatile教程

    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仍然被写入主内存,但是这一次days的值的改变在months和years写入之前,因此新值没法正确的对其他线程可见,指令重排的语义以前被改变

    Java volatile Happens-Before 保证

    happens-before 关系是程序语句之间的排序保证,这能确保任何内存的写,对其他语句都是可见的。
    为了解决指令重排挑战,volatile除了可见性保证之外,Java 关键字还提供“Happens-Before Guarantee”规则。"Happens-Before"保证:
    如果线程A写入一个volatile变量,随后线程B读取相同的变量。那么变量对线程A来说在写入变量前就是可见的,对于B来说读取完变量后,对该变量也是可见的。
    volatile变量的读和写指令不能由JVM重新排序()。读写指令前后可以重排序,但是volatile读和写不能与这些指令混合。无论什么指令都应该在volatile变量读写之后。

    volatile并不足够解决所有问题

    volatile虽然能满足直接把数据写入主内存并且直接从主内存中取出,仍然存在不足的情况
    在前面解释的情况中,只有线程1写入共享counter变量,声明该counter变量volatile足以确保线程2始终看到最新的写入值。
    实际上,如果写入volatile变量的新值不依赖于其先前的值,则多个线程甚至可以写入共享变量,并且仍然具有存储在主存储器中的正确值。换句话说,如果将值写入共享volatile变量的线程首先不需要读取其值来计算其下一个值。
    一旦线程需要首先读取volatile变量的值,并且基于该值为共享volatile变量生成新值,volatile变量就不再足以保证正确的可见性。读取volatile 变量和写入新值之间的短时间间隔会产生竞争条件 ,其中多个线程可能读取volatile变量的相同值,为变量生成新值,并在将值写回时主存 - 覆盖彼此的值。
    多个线程递增相同计数器的情况恰好是 volatile变量不够的情况。以下部分更详细地解释了这种情况。

    想象一下,如果线程1将counter值为0 的共享变量读入其CPU高速缓存,则将其增加到1并且不将更改的值写回主存储器。然后,线程2可以counter从主存储器读取相同的变量,其中变量的值仍为0,进入其自己的CPU高速缓存。然后,线程2也可以将计数器递增到1,也不将其写回主存储器。这种情况如下图所示:


    线程1和线程2现在几乎不同步。共享counter变量的实际值应为2,但每个线程的CPU缓存中的变量值为1,而主存中的值仍为0.这是一个混乱!即使线程最终将共享counter变量的值写回主存储器,该值也将是错误的。

    volatile在什么时候使用

    正如我前面提到的,如果两个线程都在读取和写入共享变量,那么使用 volatile关键字是不够的。 在这种情况下,您需要使用synchronized来保证变量的读取和写入是原子性。读取或写入volatile变量不会阻止线程读取或写入。为此,您必须在关键部分周围使用synchronized 关键字。

    作为synchronized块的替代方法,您还可以使用java.util.concurrent包中找到的众多原子数据类型之一。例如,AtomicLong或者 AtomicReference其他更多。

    如果只有一个线程读取和写入volatile变量的值,而其他线程只读取变量,那么读取线程将保证看到写入volatile变量的最新值。则可以使用volatile关键词

    volatile关键字适用于32位和64位变量。

    volatile的性能因素

    volatile变量会导致变量读取和写入主内存。读取和写入主内存比访问CPU缓存更昂贵。访问volatile变量也会阻止指令重新排序,这是一种正常的性能增强技术。因此,在真正需要强制实施变量可见性时,应该只使用volatile变量。

    相关文章

      网友评论

          本文标题:Java关键字Volatile

          本文链接:https://www.haomeiwen.com/subject/sddrwftx.html