深入理解Java多线程中的volatile关键字

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

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

实际上,从java5开始,volatile关键字就不仅仅是保证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就可以保证写操作,其他线程对这个变量的可见性 **

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.的最基本的理解

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足够了

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

volatile关键字对效率的影响

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容

  • 前言 今天介绍下volatile关键字,volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java ...
    嘟爷MD阅读 1,290评论 7 27
  • Java的关键字 volatile 用于将变量标记为“存储于主内存中”。更确切地说,对 volatile 变量的每...
    holysu阅读 5,786评论 0 13
  • 《体验》 送葬的队伍从东到西 在坡马的中心路上走过去 百家姓上的张、王、李、赵…… 都在走过去。我感到路在缩短 因...
    溪小石吴索卫阅读 91评论 1 1
  • 一 奥莉加·伊凡诺夫娜所有的朋友和熟人都出席了她的婚礼。 “你们瞧瞧:他是不是有点意思?”她对朋友们说,朝丈夫那边...
    小团阅读 762评论 0 0
  • 1.2016年中让自己可以拿到机动车驾驶证(得花时间努力去练车) 2.阅读1000本电子书(希望更好的提升自己) ...
    兔子彤阅读 622评论 0 1