Java中的Volatile关键字

Java的volatile关键字用于标记一个Java变量为“在主存中存储”。更确切的说,对volatile变量的读取会从计算机的主存中读取,而不是从CPU缓存中读取,对volatile变量的写入会写入到主存中,而不只是写入到CPU缓存。

实际上,从Java5开始,volatile关键字不只是保证了volatile变量在主存中写入和读取,我回在后面的部分做相关的解释。

变量可见性问题

Java的volatile关键字保证了多个线程对变量值变化的可见性。这听起来有点抽象,让我来详细解释。

在一个多线程的程序中,当多个线程操作非volatile变量时,出于性能原因,每个线程会从主存中拷贝一份变量副本到一个CPU缓存中。如果你的计算机有多于一个CPU,每个线程可能会在不同的CPU中运行。这意味着每个简称拷贝变量到不同CPU的缓存中,如下图:

对于非volatile变量,并没有保证何时JVM从主存中读取数据到CPU缓存,或者从CPU缓存中写出数据到主存。这会导致一些问题。

想象一种情况,多于一个线程访问一个共享对象,这个共享对象包含一个计数变量如下声明:

publicclassShareObject{publicintcounter =0;}

考虑只有一个线程Thread1增加counter这个变量的值,但是Tread1和Thread2可能有时会读取counter变量。

如果counter变量没有被声明为volatile,就不能保证何时这个变量的值会从CPU缓存写回主存,这意味着,在CPU缓存中的counter变量的值可能和主存中的不一样。如下图所示:

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

Java的volatile可见性保证

Java的volatile关键字想要解决变量可见性问题。通过声明counter变量为volatile,所有对counter变量的写入都回立即写回到主存,同时所有对counter变量也都会从主存中读取。

西面的代码展示了如何把counter变量声明为volatile:

publicclassSharedObject{publicvolatileintcounter =0;}

声明一个变量为volatile保证了对变量的写入对其他线程的可见性。

在上面的场景中,一个线程(T1)修改了counter变量的值,另一个线程(T2)读取counter变量(但是不修改它),声明counter变量为volatile足以保证对counter变量的写入对T2可见。

但是,如果T1和T2都去增加counter变量的只,name声明counter变量为volatile是不够的,后面会说明。

全volatile可见性保证

实际上,Java的volatile的可见性保证不止volatile变量本身。可见性保证如下:

如果线程A写一个volatile变量,线程B随后读取这个volatile变量,那么在写这个volatile变量之前对线程A可见的所有变量,在线程B读取这个volatile变量之后对线程B也可见。

如果线程A读取一个volatile变量,那么当A读取这个volatile变量时所有对线程A可见的变量也可以从主存中再次读取。

我用下面的代码来说明:

publicclassMyClass{privateintyears;privateintmonths;privatevolatileintdays;publicvoidupdate(intyears,intmonths,intdays){this.years = years;this.months = months;this.days = days; }}

update()方法写入三个变量,只有days变量是volatile的。

全volatile可见性保证的意思是,当一个值写入到days变量,则所有对当前线程可见的变量也会都写入到主存,也就是当一个值写入到days变量,则years和months的只也被写入到主存。

当读取years,months和days的值,可以这样做:

publicclassMyClass{privateintyears;privateintmonths;privatevolatileintdays;publicinttotalDays(){inttotal =this.days; total += months *30; total += years *365;returntotal; }publicvoidupdate(intyears,intmonths,intdays){this.years = years;this.months = months;this.days = days; }}

需要注意的是totalDays()方法起始于读取days的值到total变量中。当读取days的值时,months和years的值也被读取到主存。因此可以保证你看到的是days,months和years的最新的值,前提是保证上面的读取顺序。

指令重排序挑战

出于性能的考量,JVM和CPU允许对程序中的指令进行重排序,只要指令的语义不变。例如下面的指令:

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

这些指令可以按照下面的顺序重排,并不会丢失程序的语义:

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

但是,指令重排序对于其中一个变量是volatile变量这种情况是有挑战的。让我们看一下MyClass这个类:

publicclassMyClass{privateintyears;privateintmonths;privatevolatileintdays;publicvoidupdate(intyears,intmonths,intdays){this.years = years;this.months = months;this.days = days; }}

一旦update()方法对days变量写入一个值,years和months新写入的只也刷入到主存,但是,如果有JVM指令重排序,像下面这样:

publicvoidupdate(intyears,intmonths,intdays){this.days = days;this.months = months;this.years = years;}

months和years的只在days变量修改的情况下依然会写入到主存,但是这时将years和days变量值刷入主存这件事发生在对months和years写入新值之前,则对years和days的更新对其他线程来说就不可见了。这下指令重排序就改变了程序的语义。

Java有一个应对此问题的解决方案,下面会讲到。

Java的volatile的Happens-Before保证

为了解决指令重排序的挑战,Java的volatile关键字除了可见性保证之外,给出了一个“happens-before”的保证。happens-before保证如下情况:

如果读取和写入其他非volatile变量发生在写入volatile变量之前(这种情况这些非volatile变量也会被刷入主存),则读取和写入这些变量不能被重排序为发生在写入这个volatile变量之后(禁止指令重排序)。在写入一个volatile变量之前的读取和写入非volatile变量被保证为“happen before”写入这个volatile变量。需要注意的是,例如在写入一个volatile变量之后读写其他变量可以被重排序到写入这个volatile变量之前。从“之后”重排序到”之前“是允许的,但是从”之前“重排序到”之后“是禁止的。

如果读写其他非volatile变量发生在读取一个volatile变量之后(这种情况这些非volatile变量也会被刷到主存),则读写这些变量不能被重排序为发生在读取这个volatile变量之前。需要注意的是,读取其他变量发生在读取一个volatile变量之前能够被重排序为发生在读取这个volatile变量之后。从”之前“重排序到“之后”是允许的,但是从“之后”重排序到“之前”是被禁止的。

上面的happens-before保障保证的volatile关键字的可见性是强制的。

volatile不总是足够的

尽管volatile关键字保证了所有对一个volatile变量的读取都是从主存中读取,所有对volatile关键字的写入都是直接到主存,但是仍有其他情况使得声明一个变量为volatile是不足够的。

在前面解释的情况,也就是只有Thread1写共享变量counter,声明counter变量为volatile足以保证Thread2总是看到最新写入的值。

实际上,多线程都可以写一个共享的volatile变量,并且仍然在主存中存储正确的值,前提是写入变量的新值不依赖于它之前的值。也就是说,如果一个线程写入一个值到共享的volatile变量不需要先去读它的值去产出下一个值。

只要一个线程需要首先读取一个volatile变量的值,基于这个值生成一个新值,则一个volatile关键字不足以保证正确的可见性。在读取volatile变量然后写入新值的短暂的间隙,会产生竞态条件(race condition),这时多个线程可能读取到相同的volatile变量的值,生成这个变量的新值,当将新值写回主存时,会覆盖彼此的值。

多线程增加相同计数器的值就是这种情况,导致一个volatile声明不足够。下面详细解释这种情况。

想象如果Thread1读取一个值为0的共享的counter变量到它的CPU缓存,增加1并且不将这个改变的值写回主存。Thread2然后从主存中读取相同的值仍为0counter变量到它的CPU缓存。Thread2也为它增加1,也不写回主存。这种情况如下图所示:

Thread1和Thread2此时实际上已经不同步了。共享变量counter的值应该为2,但是每个线程在CPU缓存中的这个变量的值都为1,在主存中的值仍为0,这就乱了!尽管这两个线程最终会将值写回主存中的共享变量,这个值也是不正确的。

何时volatile是足够的?

正如前面所说,如果两个线程都去读写同一个共享变量,只对这个共享变量使用volatile关键字是不够的。你需要使用一个 synchronized 关键字去保证读写相同变量是原子的。读写一个volatile变量不会阻塞线程的读写。

作为synchronized块替代方法,你可以使用 java.util.concurrent 包中的众多原子数据类型。比如,AtomicLong或者AtomicReference或其他的类型。

只有一个线程读写一个volatile变量值,其他线程只读取变量,则这些读线程能够保证看到写入这个volatile变量的最新值,如果不声明为volatile,则这种情况不能保证。

volatile的性能考量

读写volatile变量会导致变量被读写到主存。读写主存比访问CPU缓存开销更大。访问volatile变量也会禁止指令重排序,而指令重排序是一个正正常的性能优化技术。因此,你应该只在真正需要保证变量可见性的时候使用volatile变量。

需要java学习路线图的私信笔者“java”领取哦!另外喜欢这篇文章的可以给笔者点个赞,关注一下,每天都会分享Java相关文章!还有不定时的福利赠送,包括整理的学习资料,面试题,源码等~~

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

推荐阅读更多精彩内容