当前位置: 澳门新濠3559 > 编程 > 正文

这样做是为了保证读取该变量的信息都是最新的

时间:2019-11-21 05:33来源:编程
  以下内容转自(使用谷歌翻译): Java 的 volatile关键字对可见性的保证 volatile多用于多线程的环境,当一个变量定义为volatile时,读取这个变量的值时候每次都是从momery里面读取而不

 

以下内容转自(使用谷歌翻译):

  • Java 的 volatile关键字对可见性的保证

volatile多用于多线程的环境,当一个变量定义为volatile时,读取这个变量的值时候每次都是从momery里面读取而不是从cache读。这样做是为了保证读取该变量的信息都是最新的,而无论其他线程如何更新这个变量。

Java volatile关键字用于将Java变量标记为“存储在主存储器”中。更准确地说,这意味着,每个读取volatile变量将从计算机的主存储器中读取,而不是从CPU缓存中读取,并且每个写入volatile变量的写入将被写入主存储器,而不仅仅是写入CPU缓存。

  • Java 的 volatile关键字在保证可见性之前的所做的事情
  • 为什么volatile关键字有时候也不是足够的
  • 什么时候volatile足够了
  • volatile关键字对效率的影响

实际上,由于Java 5的volatile关键字保证不仅仅是volatile变量被写入和从主内存读取。我将在以下各节中解释一下。

Java关键字用于将一个变量标记为“存储在内存中的变量”。更准确的说,意思就是每一次对volatile标记的变量进行读取的时候,都是直接从电脑的主内存进行的,而不是从cpu的cache中,而且每个对volatile变量的写入操作,都会被直接写入到主存里,而不是只写到cache里。

Java volatile可见性保证

实际上,从java5开始,volatile关键字就不仅仅是保证volatile变量从主存读写,笔者会在后面详细讨论这个问题。

Java volatile关键字可确保跨线程对变量的更改的可见性。这可能听起来有点抽象,所以让我详细说明一下。

Java 的 volatile关键字对可见性的保证

Java的volatile关键字可以保证变量的可见性。说起来很简单,但具体是什么意思呢?

在多线程的应用程序中,线程操作非volatile的变量,为了更快速的执行程序,每个线程都会将变量从主存复制到cpu的cache中。如果你的电脑有多个cpu,每个线程都在不同的cpu上运行,这就意味着,每个线程将变量的值复制到不同的cpu的cache上,就像下面这个图所表明:

Paste_Image.png

如果变量没有声明为volatile,那么就无法知道,变量什么时候从主存中读取到cpu的cache中,有什么时候从cache中写回到主存中。这就可能造成很多潜在的问题:

假设一种情况,多个线程同时持有一个共享对象的引用,这个对象包括一个counter变量:

public class SharedObject {

    public int counter = 0;

}

假设这种情况,只有线程1自增了这个counter变量,但是线程1和线程2可能随时读取这个counter变量。如果这个counter变量没有被声明为volatile,那么就无法确认,什么时候counter的变量的值会从cpu的cache中写回到主存中,这就意味着,counter变量的值在cpu的cache中的值可能和主存中不一样,如下图所示:

Paste_Image.png

这个线程的问题无法及时的看到变量的最新的值,因为可能这个变量还没有被另一个线程写回到主存中。所以一个线程对一个变量的更新对其他的线程是不可见的。这就是我们最初提出的线程的可见性问题。

通过将一个变量声明为volatile,那么所有对这个变量写操作会被直接写回到主内存中,所以这对线程都是可见的。而且,所有对这个变量的读取操作,也会直接从主存中读取,下面说明了如何声明一个voaltile变量:

public class SharedObject {

    public volatile int counter = 0;

}

** 将一个变量声明为volatile就可以保证写操作,其他线程对这个变量的可见性 **

出于性能原因,线程在非volatile变量上运行的多线程应用程序中,每个线程可能会将变量从主存储器复制到CPU高速缓存中。如果你的计算机包含多个CPU,则每个线程可能在不同的CPU上运行。这意味着每个线程都可以将变量复制到不同CPU的CPU缓存中。这在这里说明了:

Java 的 volatile关键字在保证可见性之前的所做的事情

从java5开始,volatile关键字不仅可以保证变量直接从主内存中读取,还有一下作用:

  • 如果线程A对一个volatile变量进行写操作,线程B随后读取同一个volatile值,那么在线程将变量写操作完成之后的所有变量对线程A和B都是可见的。
  • 那些操作volatile变量的读写指令的顺序无法被JVM改变(JVM有时候为了效率会改变变量读写顺序,只要JVM判断改变顺序对程序没有影响的话)。

上面两段话不是很理解,我们接下来进行一个更细致的说明:

当一个线程对一个volatile变量进行写操作的时候,不仅仅是这个变量自己被写入到主存中,同时,其他所有在这之前被改变值的变量也都会线程先写入到主存中。
当一个线程对一个volatile变量进行读取操作,他也会将所有跟着那个volatile变量一起写入到主存中的其他所有变量一起读出来。
看下面这个例子:

Thread A:
    sharedObject.nonVolatile = 123;
    sharedObject.counter     = sharedObject.counter + 1;

Thread B:
    int counter     = sharedObject.counter;
    int nonVolatile = sharedObject.nonVolatile;

因为线程A在对volatile的sharedObject.counter进行写操作之前,先对sharedObject.nonVolatile变量进行写操作,所以当线程A要将volatile的sharedObject.counter写回到主存时,这两个变量都会被写回到主存中。

同理,线程B在读取volatile变量到sharedObject.counter的时候,两个变量sharedObject.counter and sharedObject.nonVolatile所以线程读取变量sharedObject.nonVolatile就会看到他被线程A改变后的值。

开发者可以利用这个扩展的可见性去放大线程间的变量可见性,不需要将每一个变量都声明为volatile,只需要声明一两个变量为volatile就可以了。下面这个简单的例子,就来说明这个问题:

public class Exchanger {

    private Object   object       = null;
    private volatile hasNewObject = false;

    public void put(Object newObject) {
        while(hasNewObject) {
            //wait - do not overwrite existing new object
        }
        object = newObject;
        hasNewObject = true; //volatile write
    }

    public Object take(){
        while(!hasNewObject){ //volatile read
            //wait - don't take old object (or null)
        }
        Object obj = object;
        hasNewObject = false; //volatile write
        return obj;
    }
}

线程A可能会调用put方法将objects put进去,线程B可能会调用take方法将object拿出来。这个类可以正常工作,只要我们使用一个volatile变量即可(不使用同步语句),只要只有线程A调用put,只有线程B调用take。

然后,JVM有时候为了提高效率,可能会改变指令执行的顺序,只要JVM判断这样做不改变指令的语义,那么就有可能改变指令的顺序。那么如果JVM改变了指令的执行顺序会发生什么呢?put方法可能会像下面这样执行:

while(hasNewObject) {
    //wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;

我们观察到,现在对于volatile的hasNewObject 操作在object = newObject;之前执行,这说明,object还没有真正被赋值新对象,但是hasNewObject 已经先变为true了。对于JVM来说,这种交换是完全有可能的。因为这两个write的指令彼此不是互相依赖的。

但是这样交换顺序之后可能会对object变量的可见性产生不好的影响。首先,线程B可能会在线程A真正给object写入一个新值之前,就看到hasNewObject 变为true。
另一方面,我们无法确保object什么时候会被真正写入到主内存中。

为了防止上面这种情况的发生,volatile关键字就提出了一种“happens before guarantee”,这可以保证volatile的变量的读写指令不会被重新排序。指令前面的和后面的可以随意排序,但是volatile变量的读写指令的相对顺序是不能改变的。

看下面这个例子就能理解了:

sharedObject.nonVolatile1 = 123;
sharedObject.nonVolatile2 = 456;
sharedObject.nonVolatile3 = 789;

sharedObject.volatile     = true; //a volatile variable

int someValue1 = sharedObject.nonVolatile4;
int someValue2 = sharedObject.nonVolatile5;
int someValue3 = sharedObject.nonVolatile6;

JVM可能会改变前三个指令的顺序,只要他们在volatile的写指令之前发生(就是说他们必须在volatile的写指令之前发生)。
同理,JVM也可能改变后三个指令的顺序,只要他们在volatile的写指令之后发生。

这就是对于Java的 volatile happens before guarantee.的最基本的理解

澳门新濠3559 1

Volatile有时候也是不够的

虽然volatile可以保证读取操作直接从主内存中的读取,写操作直接写到内存中,但仍然存在一些情况下,光使用volatile关键字是不够的。

在之前的举例的程序中,只有一个线程在向共享变量写入数据的时候,声明为volatile,另一个线程就可以一直看到最新被写入的值。

实际上,只要新值不依赖旧值的情况下,多个线程同时向共享的volatile变量里写入数据时,仍然能在主内存中得到正确的值。换句话说,就是这个volatile变量值更新的时候,不需要先读取出他以前的值才能得到下一个值。

只要一个线程需要先读取一个voaltile变量,然后必须基于他的值才能产生新的值,那么volatile关键字就不再能保证变量的可见性了。在读取变量和写入变量的时候,存在一个短的时间间隙,这就会造成,多个线程可能会在这个间隙读取同一个值,产生一个新值,然后写入到主内存中,将其他线程对值的改变给覆盖了。

所以常见的情况就是如果一个volatile变量在进行自增或者自减操作,那么这时候使用volatile就可能出问题。
接下来我们更深入的讨论这个问题,假设线程1读取一个共享的counter变量到cpu的cache中,此时他的值是0,然后给它自增加一,但是还没有写到主存中,所以主存中还是1,线程2也能够读取同一个counter变量,而这个变量读取的时候还是0,在他自己的cpucache中,这样就出现问题了:

Paste_Image.png

线程1和线程2实际上是不同步的。共享变量counter的真实值实际上应该为2,因为被加了两次,但是每个线程在自己的cache上存的值是1,而且在主存中这个值仍然是0,这就变得很混乱。即使线程最后将值写回到主存中,但最后的值也是不正确的。

对于非volatile变量,不能保证Java虚拟机(JVM)将数据从主存储器读取到CPU高速缓存中,或者将数据从CPU缓存写入主存储器。这可能会导致几个问题,我将在以下部分中解释。

什么时候volatile足够了

前文中提到,如果两个线程都在对volatile变量进行读写操作,那么仅仅使用volatile关键字是远远不够的。你需要使用synchronize关键字,来保证读写操作的原子性。
但如果是只有一个线程在读写volatile变量,另外的多个线程仅仅是读取这个变量的话,那么这就可以保证,其他读线程所看到的变量值都是最新的。volatile关键字可以使用在32位或者64位的变量上。

想象一下,两个或多个线程可以访问共享对象的情况,该对象包含一个如下所示的计数器变量:

volatile关键字对效率的影响

读写一个volatile变量的时候,会导致变量直接在主存中读写,显然,直接从主存中读写速度要比从cache中来得慢。另一方面,操作volatile变量的时候不能改变指令的执行顺序,这一定程度上也会影响读写的效率。所以,只有我们需要确保变量可见性的时候,才会使用volatile关键字。

public class SharedObject {

    public int counter = 0;

}

想象一下,只有线程1增加counter变量,但线程1和线程2都可能对counter不时读取变量。

如果counter未声明变量,volatile则不能保证将counter变量的值从CPU缓存写回主存储器。这意味着counter在CPU缓存中的变量值可能与主内存不一样。这种情况在这里说明:

澳门新濠3559 2

没有看到变量的最新值,因为还没有被另一个线程写回到主内存的线程的问题被称为“可见性”问题。一个线程的更新对其他线程是不可见的。

通过声明counter变量,对变量的volatile所有写入counter将立即写回主内存。此外,counter变量的所有读取将直接从主存储器读取。下面是如何volatile在声明counter 变量的样子:

public class SharedObject {

    public volatile int counter = 0;

}

因此, 声明一个volatile变量可以保证该变量的其他写入线程的可见性。

Java volatile事件保证

由于Java 5的volatile关键字不仅仅保证了对变量的主内存的读取和写入。实际上,volatile关键字保证:

  • 如果线程A写入volatile变量和线程B随后读取相同的volatile变量,然后看到线程A的所有变量之前写volatile变量,也将是可见的线程B后,它已经读volatile变量。 

  • volatile变量的读写指令不能被JVM重新排序(只要JVM从重新排序中没有检测到程序行为的变化,JVM可能会因为性能原因重新排序指令)。之前和之后的指令可以重新排序,但是这些指令不能混合写入或写入。无论读取还是写入volatile变量,任何指令都将保证在读取或写入后发生。

这些陈述需要更深入的解释。

当一个线程写入一个volatile变量时,不仅将volatile变量本身写入主存储器。在写入volatile变量之前,线程更改的所有其他变量也被刷新到主存储器。当一个线程读取一个volatile变量时,它也将从主存储器中读取与volatile变量一起刷新到主存储器的所有其他变量。

看这个例子:

Thread A:
    sharedObject.nonVolatile = 123;
    sharedObject.counter     = sharedObject.counter + 1;

Thread B:
    int counter     = sharedObject.counter;
    int nonVolatile = sharedObject.nonVolatile;

由于线程A在写入volatile变量sharedObject.counter之前写入非volatile变量sharedObject.nonVolatile,所以当线程A写入sharedObject.counter(volatile变量)时,sharedObject.nonVolatile和sharedObject.counter都将写入主内存。

由于线程B从读取volatile的sharedObject.counter开始,所以sharedObject.counter和sharedObject.nonVolatile都从主内存读取到线程B使用的CPU高速缓存。当线程B读取sharedObject.nonVolatile时,它会看到值由线程A写。

开发人员可以使用这种扩展的可见性保证来优化线程之间变量的可见性。而不是声明每个变量volatile,只需要声明一个或几个变量volatile。这是一个简单的Exchanger类的例子:

public class Exchanger {

    private Object   object       = null;
    private volatile hasNewObject = false;

    public void put(Object newObject) {
        while(hasNewObject) {
            //wait - do not overwrite existing new object
        }
        object = newObject;
        hasNewObject = true; //volatile write
    }

    public Object take(){
        while(!hasNewObject){ //volatile read
            //wait - don't take old object (or null)
        }
        Object obj = object;
        hasNewObject = false; //volatile write
        return obj;
    }
}

线程A可能会通过调用put()来不时地设置对象。线程B可能会通过调用take()来不时地获取对象。只要线程A调用put()并且只有线程B调用take(),这个Exchanger可以使用volatile变量(不使用同步块)来正常工作。

但是,如果JVM可以在不改变重新排序的指令的语义的情况下,JVM可以重新排序Java指令来优化性能。如果JVM切换的读取和顺序写入里面会发生什么,put()take()?如果put()真的执行如下:

while(hasNewObject) {
    //wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;

注意,在实际设置新对象之前,对volatile变量hasNewObject的写入将被执行。对于JVM,这可能看起来完全有效。两个写入指令的值不依赖于彼此。

但是,重新排序指令执行会损害对象变量的可见性。首先,线程B可能会在线程A实际上为对象变量写入一个新值之前看到hasNewObject设置为true。第二,现在甚至不能保证写入对象的新值将被刷新回主内存(以下是线程A在某处写入volatile变量的情况)。

为了防止上述情况发生,volatile关键词带有“在保证之前发生”。在保证之前发生的事件保证了易失性变量的读写指令无法重新排序。之前和之后的指令可以重新排序,但是无法通过在其之前或之后发生的任何指令来重新排序易失性读/写指令。

看这个例子:

sharedObject.nonVolatile1 = 123;
sharedObject.nonVolatile2 = 456;
sharedObject.nonVolatile3 = 789;

sharedObject.volatile     = true; //a volatile variable

int someValue1 = sharedObject.nonVolatile4;
int someValue2 = sharedObject.nonVolatile5;
int someValue3 = sharedObject.nonVolatile6;

只要所有这些指令都发生在易失性写入指令之前(它们必须在易失性写入指令之前都必须执行),JVM可能会重新排序前3个指令。

类似地,只要在所有这些指令之前发生易失性写入指令,JVM可以重新排序最后3条指令。在易失性写入指令之前,最后3条指令都不能重新排序。

这基本上是Java保护之前发生的volatile的意思。

volatile并不总是足够

即使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,也不会将其写回主存储器。这种情况如下图所示:

澳门新濠3559 3

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

什么时候使用呢?

如前所述,如果两个线程都是共享变量的读取和写入,则使用volatile关键字是不够的。 在这种情况下,你需要使用synchronized来保证变量的读写是原子的。读取或写入volatile变量不阻止线程读取或写入。为了实现这一点,你必须在关键部分周围使用synchronized关键字。

作为synchronized块的替代,你还可以使用java.util.concurrent澳门新濠3559,包中发现的许多原子数据类型之一。例如,AtomicLong或 AtomicReference其他人之一。

如果只有一个线程读写volatile变量的值,并且其他线程只读取变量,则读取线程将被保证看到写入volatile变量的最新值。在不变量变动的情况下,这不能保证。

volatile关键字保证在32位和64变量上工作。

性能考虑波动

读写volatile变量会导致变量被读取或写入主存储器。读取和写入主内存比访问CPU缓存更昂贵。访问volatile变量还可以防止指令重新排序,这是正常的性能增强技术。因此,当你真正需要强制实现变量的可见性时,你应该只使用volatile变量。

编辑:编程 本文来源:这样做是为了保证读取该变量的信息都是最新的

关键词: