Jakob Jenkov多线程系列一一Java Volatile Keyword

Java Volatile Keyword

Java中的volatitle关键字的作用是使一个Java变量"被放置在主存区中",说得更确切一点是:每一个volatile变量只能从内存中被读到,而不是从CPU缓存中,与此相对的是每一次写入操作也会使变量被写到内存中,而不是CPU缓存中。
在Java5.0以后的版本中,volatitle变量不仅仅用于读写操作了,下文会给出解释。

The Java volatile Visibility Guarantee

Java中的volatitle关键字通过线程来保证一个变量的可见性,这听起来可能有点玄幻。
在一个所有线程都不包含volatile关键字的程序中,每一个线程在操作变量时都能将其从主存区中复制到缓存。出于性能原因,如果你的电脑包含多个CPU,每个线程可能运行在不同的CPU上,这意味着每个线程可能复制同一个变量到不同的CPU缓存上,如下图所示:

由图中我们可以了解到,当虚拟机从主存区读数据到CPU缓存,或者从CPU缓存写数据到主存区中时,如果没有volatitle语句意味着安全性得不到保证,并可能会导致一些问题,下面举例说明:
当两个或更多线程同时进入一个包含一个计数变量的类中时:
public class SharedObject {
public int counter = 0;
}
此时线程1在增加这个conunter变量,于此同时线程2可能在一次又一次的读这个变量。
如果变量counter没有volatitle关键字,那么当counter的值从缓存被写入到主存区时,就不能保证线程安全性。这也就意味着:在CPU缓存和主存区中的counter变量的值可能会不一样,如下图所示:
由于另一个线程没有写入主存区而导致当前线程没有读到变量当前值所导致的问题被称为"可见性问题",也就是说一个线程更新的值不能被其他线程看到。
解决办法是给这个counter变量加上volatitle关键字,之后所有的写入操作都会被立即写入主存区,与此相对应的,所有的读出操作也都直接从主存区中读,如下所示:
public class SharedObject {

    public volatile int counter = 0;

}
通过设置volatitle关键字使得对其他写入改变量的线程的可见性得到了保证。

The Java volatile Happens-Before Guarantee

自从Java5.0 volatile关键字出现以后,不只是保证了变量能够被读写到主存区中,实际上还保证了如下功能:
1、如果同一时间线程A和B分别对一堆变量进行写和读操作,那么执行写入操作之前这些volatile变量对于A线程来说是可见的,与此相对,在执行读出操作之后这些volatile变量对于线程B来说是可见的。
2、对volatile变量的读写操作不能被JVM重排序(JVM在不影响程序工作的情况下可能出于性能原因对指令进行重排),在volatile之前或之后的指令都有可能被重新排序,但是volatitle变量的读写指令不会被混到这些指令里去,在volatitle读写之后的读写指令保证会在volatitle读写之后执行。
下面的部分需要更深层次的理解:
当一个线程写入一个volatile变量时,不仅仅这个volatile变量本身被写到主存区中,在此之前所有的被该线程改变过的变量都会被怼到主存区中去。
当一个线程读到一个volatile变量时它也会读到所有与这个volatitle一起被怼进来的变量。
举个例子:
Thread A:
    sharedObject.nonVolatile = 123;
    sharedObject.counter     = sharedObject.counter + 1;

Thread B:
    int counter     = sharedObject.counter;
    int nonVolatile = sharedObject.nonVolatile;
其中sharedObkect.nonVolatile是非volatile值,sharedObject.counter是volatile值。
因为线程A在写入sharedObkect.counter之前写入了sharedObkect.nonVolatile的值,所以在A写入sharedObkect.counter的同时sharedObkect.nonVolatile与sharedObkect.counter都被写入到了主存区中。
同理可得,在线程B中因为counter先被读到缓存中,所以当B读到sharedObkect.nonVolatile时可以看到该变量被A改动的值(可见性)。
开发者可以使用这个扩展的可见性保证技巧来优化线程间变量的可见性,只给一小部分变量加上volatile,而不是给每个变量都加上volatile。
下面是一个体现该原理的交换机类:
public class Exchanger {

    private Object   object       = null;
    private volatile boolean 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()方法,在这种情况下只能通过volatitle变量来保证该类运作正常。(同步锁并不能做到这一点)。
因为JVM可能出于性能考虑对指令进行重排序,而同步锁并不能保证这种情况下指令的执行顺序,设想一下这种情况下put()和take()方法的执行顺序?
如果put()方法的执行情况如下所示:
while(hasNewObject) {
    //wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;
这里要注意到volatitle变量hasNewObject是在设置新的object值之前执行的,对于JVM来说这段代码是没问题的,因为这两个写入指令互不影响。

然而,对指令的重排可能会影响object变量的可见性。首当其冲的是线程B可能会在线程A给object设置新的值之前看到hasNewObeject已经被设置为true,这样B就执行不下去了。其次,也没有能保证被写到object的新的值在什么时候会被flushback到主存区中。

为了防止以上情况的发送,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变量之前。(也就是说,它们一定会在volatile关键字的写入操作之前执行)

在volatile写入指令执行之后,JVM可能会对剩下的三条指令进行重排。剩下的三条绝对不会在volatile写入操作执行之前被重排。

以上就是"出现顺序决定执行顺序"特性的基本含义。

volatile is Not Always Enough

即使volatile关键字保证了一定直接从主存区中读数据,一定写入数据到主存区中,但是仍然有些情况是仅仅定义volatile变量不够解决的。
在前面解释的情况中,线程1写入数据到共享的counter变量中,声明volatile变量足以保证counter变量的改动会被线程2看到。
但事实上,多线程能够同时对一个volatile数据进行写入操作,并且能够保证正确的值被存储到主存区中,只要这个被写入到该变量的新值是不依赖以前的值的。换句话说,这种情况是:如果一个线程写入一个值到共享的volatile变量中,并不需要知道它上一个值是什么。
如果一个线程需要首先读取一个volatile变量的值,并且基于该值为共享volatile 量生成一个新的值,在这种情况下一个volatile变量就不足以保证可见性了 当多个线程读或写到一个相同的volatile变量,为了保证可见性而产生的在很短间隔内的对volatile的读写操作会产生一种竞争。
多个线程同时执行一个计数器的递增正是一种这样的情况,一个volatile变量是不够的,以下部分将更详细地解释这种情况。
想象一下:当线程1读到变量counter的值为0,并读到缓存中,对其进行增加1的操作,而此时这个改变还没有写入到主存区。线程2能够读到的counter的值在主存区中还仍然是0,于是线程2又将这个值读到缓存中,进行+1操作,并且也还没有写入到主存区中去。情况如下图所示:
线程1和线程2几乎是同步执行的,此时真正的counter的值应该是2,然而每一个线程带有的缓存中的该变量的值都是1,主存区中的值仍然是0,真是GG啊!
即使最终两个线程都将各自的值写入了主存区中,结果也是错的。

When is volatile Enough?

正如我之前所提到的,如果两个线程同时对一个变量进行读写操作,那么此时volatile关键字就不够了,需要和synchronized关键字去保证其原子性。
对volatile变量的读或者写并不限制线程读写。为了实现这一点你必须用synchronized关键字在特定位置。
同样为了实现这种同步锁的效果你也能使用java.util.concurrent package包中的一些原子类,例如AtomicLong或者AtomicReference或者其他的。
当一个线程对一个volatile变量进行读写操作,而其他线程只进行读操作时,此时进行读操作的线程被保证能看到最新的写入到该volatile参数的值,如果不使用volatile参数,这将不能得到保证。

Performance Considerations of volatile


因为volatile关键字会导致该变量读写在主存区中,比起普通的存到缓存区是更加消耗性能的。
通过设置volatile关键字防止指令重排也是提高性能的一种手段。
所以,你应该只在必须要可见性的时候才设置volatile关键字,不能滥用。



*********************************************************************************分割线**************************************************************
第一次发译文,有很多地方翻译得可能不是很透彻,这里附上原文地址:http://tutorials.jenkov.com/java-concurrency/volatile.html
希望各位大牛能够指出以使我能够改正错误,谢谢!

发布了70 篇原创文章 · 获赞 75 · 访问量 27万+

猜你喜欢

转载自blog.csdn.net/qq_22770457/article/details/52303863
今日推荐