volatile关键字

volatile关键字

Java关键字volatile用于将一个Java变量标记为在主内存中存储 ,更准确的解释为:每次读取一个volatile变量时将从电脑的主内存中读取而不是从CPU缓存中读取,每次对一个volatile变量进行写操作时,将会写入到主内存中而不是写入到CPU缓存中。

volatile内存可见性

volatile关键字确保了volatile变量的修改在多线程中是可见的。这听起来有些抽象,下面详细叙述。

在一个对非volatile变量进行操作的多线程应用,由于性能的关系,当对这些变量进行读写时,每个线程都可能从主线程中拷贝变量到CPU缓存中。如果你的电脑不止一个CPU,每个线程可能会在不同的CPU上运行。这意味着,每个线程都可能将变量拷贝到不同CPU的CPU缓存中,如下图

假设两个或更多的线程对下面这个包含一个计数器的共享变量拥有访问权限:

public class SharedObject {
    public int counter = 0;
}

再次假设,只有Thread1会增加 counter 变量的值,但是Thread1和Thread2都能在任意时刻读取 counter 变量的值。

如果 couner 变量没有声明为volatile将无法保证在何时把CPU缓存中的值写入主内存中。这意味着 counter 变量在CPU缓存中的值可能会与主内存中的值不一样,出现下面错误情况:

volatile 

造成线程不能获取变量最新值的原因为变量值没有被其它线程及时写回主内存中,这就是所谓的可见性问题。某个线程的更新对其它线程不可见。

将 counter 变量声明为volatile之后,所有对 counter 变量的写操作会立即写入主内存中,同样,所有对 counter 变量的读操作都会从主内存中读取数据。

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

因此定义一个volatile变量可以保证写变量的操作对于其它线程可见。

 

volatile先行规则

Java5之后volatile关键字不仅能用于确保变量从主内存中读取和写入,事实上,volatile关键字还有如下作用

1. 如果线程A写入了一个volatile变量然后线程B读取了这个相同的volatile变量,那么所有在线程A写之前对其可见的变量,在线程B读取这个volatile之后也会对其可见。

2. volatile变量的读写指令不能被JVM重排序(出于性能的考虑,JVM可能会对指令重排序如果JVM检测到指令排序不会对程序运行产生变化)。 前后的指令可以重排序,但是volatile变量的读和写不能与这些重排序指令混在一起。任何跟随在volatile变量读写之后的指令都会确保只有在变量的读写操作之后才能执行。

当一个线程向一个volatile变量写操作,此时不仅这个volatile变量自身会写入主内存,所有这个volatile变量写入之前受影响发生改变的变量也会刷写入主内存。当一个线程向一个volatile变量读操作时它同样也会从主内存中读取所有和这个volatile变量一起刷写入主内存的变量。

可以利用这个扩展的可见性来优化线程之间变量的可见性。不同于把每个变量都设置为volatile,此时只有少部分变量需要声明为volatile。

面是一个利用此规则编写的简单示例程序 Exchanger:

/**
 * @author perist
 * @date 2016/12/3
 * @time 22:15
 */
public class Exchanger {
private Object object = null;
private volatile boolean hasNewObject = false;

public void put(Object newObject) {
    while (hasNewObject) {
        //等待,不覆盖已经存在的新对象
    }
    object = newObject;
    hasNewObject = true; //volatile写入
}

public Object take() {
    while (!hasNewObject) { //volatile读取
        //等待,不获取旧的对象(或null对象)
    }
    Object obj = object;
    hasNewObject = false; //volatile写入
    return obj;
}

}

线程A随时可能会通过调用 put() 方法增加对象,线程B随时可能会通过调用 take() 方法获取对象。只要线程A只调用 put() ,线程B只调用 take() ,这个 Exchanger 就可以通过一个volatile变量正常工作,而不用通过synchronized同步代码块。

然而,JVM可能会重排序Java指令来优化性能,如果JVM可以通过不改变这些重排序指令的语义来实现此功能。如果JVM调换了 put() 和 take() 中的读和写的指令,会发生什么呢?如果 put() 真的像下面这样执行会出现什么情况呢?

    

public void put(Object newObject) {
while (hasNewObject) {
//等待,不覆盖已经存在的新对象
}
hasNewObject = true; //volatile写入
object = newObject;

}

请注意此时对于volatile变量 hasNewObject 的写操作会在新变量的实际设置前先执行,而这在JVM看来可能会完全合法。两个写操作指令的值不再依赖于对方。

但是,对于执行指令重排序可能会损害 object 变量的可见性。首先,线程B可能会在线程A对 object 真实的写入一个值到object之前读取到 hasNewObject 的值为true。其次,现在甚至不能保证什么时候写入 object 的新值会刷写入主内存。

为了阻止上面所述的这种情况发生,volatile关键字提供了一个 先行发生原则。先行发生保证确保对于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条指令,只要它们都先发生于volatile写指令。

同样的,JVM可能会重新排序最后3条指令,只要volatile写指令先行发生于它们,这3条指令都不能被重新排序到volatile指令的前面。这就是volatile先行发生原则的基本含义。

volatile同步缺陷

尽管volatile关键字确保了所有对于volatile变量的读操作都是直接从主内存中读取的,所有对于volatile变量的写操作都是直接写入主内存的,但仍有一些情况只定义一个volatile变量是不够的。

在前面的场景中,线程1对共享变量counter写入操作,声明 counter 变量为volatile之后就能够确保线程2总是可以看见最新的写入值。

事实上,如果写入该变量的值不依赖于它前面的值,多个线程甚至可以在写入一个共享的volatile变量时仍然能够持有在主内存中存储的正确值。换句话解释为,如果一个线程在写入volatile共享变量时,不需要先读取该变量的值以计算下一个值

一旦一个线程需要首先读取一个volatile变量的值,然后基于该值产生volatile共享变量的下一个值,那么该volatile变量将不再能够完全确保正确的可见性。在读取volatile变量和写入它的新值这个很短的时间间隔内,产生了一个 竞争条件 :多个线程可能会读取volatile变量的相同值,然后产生新值并写入主内存,这样将会覆盖互相的值。

这种多个线程同时增加相同计数器的场景正是volatile变量不适用的地方。

如下图:

volatile2

假设线程1读取一个值为0的共享变量 counter 到它的CPU缓存中,将它加1但是并没有将增加后的值写入主内存中。线程2可能会从主内存中读取同一个 counter 变量,其值仍然为0,同样不将其写入主内存中,程1和线程2现在都没有同步,共享变量 counter 的真实值应该是2,但是在每个线程的CPU缓存中,其值都为1,并且主内存中的值仍然是0。它成了一个烂摊子,即使这些线程终于它们对共享变量 counter 的计算值写入到主内存中,counter 的值仍然是错的。

volatile适用场景

就像在前面提到的那样,如果两个线程同时对一个共享变量进行读和写,那么仅用volatile变量是不够的。在这种情况下,你需要使用synchronized来确保关于该变量的读和写都是原子操作。读或写一个volatile变量时并不会阻塞其它线程对该变量的读和写。在这种情况下必须用synchronzied关键字来修饰你的关键代码。

除了使用synchronzied之外,你也可以使用 java.util.concurrent 包中的一些原子数据类型,如 AtomicLong , AtomicReference 等。

当只有一个线程对一个volatile变量进行读写而其它线程只读取该变量时,volatile可以确保这些读线程读取到的是该变量的最新写入值。如果不声明该变量为volatile,则不能保证这些读线程保证读取的是最新写入值。

volatile变量通常用做某个操作完成、 发生中断或者状态的标志。

volatile的语义不足以确保递增操作(counter++) 的原子性, 除非你能确保只有一个线程对变量执行写操作。  

volatile与加锁机制的主要区别是: 加锁机制既可以确保可见性又可以确保原子性, 而volatile变量只有确保可见性

volatile性能

由于volatile变量的读和写都是直接从主内存中进行的,相对于CPU缓存,直接对主内存进行读写代价更高, 访问一个volatile变量也会阻止指令重新排序,而指令排序也是一个常用的性能增强技术。

因此,你应该在只有当你确实需要确保变量可见性的时候才使用volatile变量。

0 0 投票数
Article Rating
订阅评论
提醒
guest
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x