关于关键字Volatile的理解

Java Volatile Keyword

在这篇文章中,我们将关注Java语言中的基本但经常被误解的概念 - volatile关键字。

1.概述

在Java中,每个线程都有一个独立的内存空间,称为工作内存; 这保存了用于执行操作的不同变量的值。在执行操作之后,线程将变量的更新值复制到主存储器,并且从那里其他线程可以读取最新值。

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

简单地说,volatile关键字标记一个变量,在多个线程访问它的情况下,总是转到主内存,读取和写入。

实际上,自Java 5以来,volatile关键字保证的不仅仅是易失性变量被写入主内存并从主内存中读取。我将在以下部分解释。

可变可见性问题

Java volatile关键字保证可以跨线程查看变量的变化。这可能听起来有点抽象,所以让我详细说明。

在线程操作非易失性变量的多线程应用程序中,出于性能原因,每个线程可以在处理它们时将变量从主内存复制到CPU缓存中。如果您的计算机包含多个CPU,则每个线程可以在不同的CPU上运行。这意味着,每个线程可以将变量复制到不同CPU的CPU缓存中。这在这里说明:

线程可以保存CPU缓存中主存储器的变量副本。

对于non-volatile变量,无法保证Java虚拟机(JVM)何时将数据从主内存读入CPU缓存,或将数据从CPU缓存写入主内存。这可能会导致几个问题,我将在以下部分中解释。

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

public class SharedObject {
    public int counter = 0;
}

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

如果counter变量未声明为volatile,则无法保证何时将counter变量的值从CPU缓存写回主内存。这意味着counter变量在CPU缓存中的变量值可能与主存储器中的变量值不同。这种情况如下所示:

线程1和主内存使用的CPU缓存包含计数器变量的不同值。

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

2.易失性和线程同步

对于所有多线程应用程序,我们需要确保一致的行为规则:

  • 相互排斥 - 一次只有一个线程执行一个关键部分
  • 可见性 - 一个线程对共享数据所做的更改对其他线程可见,以维护数据一致性

同步方法和块提供上述两种属性,但代价是应用程序的性能。

Volatile是一个非常有用的原语,因为它可以帮助确保数据变化的可见性方面,当然,不提供互斥。因此,它在我们可以使用多个线程并行执行代码块但我们需要确保可见性属性的地方很有用。

Java易失性可见性保证

Java volatile关键字旨在解决可变可见性问题。通过声明counter变量为volatile,对counter变量的所有写操作都将立即写回内存。此外,counter变量的所有读取都将直接从主存储器中读取。

以下是 变量volatile声明的counter样子:

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

声明volatile变量可以保证对该变量的其他写入线程的可见性。

在上面给出的场景中,一个线程(T1)修改计数器,另一个线程(T2)读取计数器(但从不修改它),声明该counter变量volatile后足以保证counter变量的写入对T2是可见的。

但是,如果T1和T2都在增加counter变量,那么声明 counter变量volatile就不够了。稍后会详细介绍。

完全不稳定的可见性保证

实际上,Java volatile的可见性保证超出了volatile 变量本身。能见度保证如下情形:

如果线程A写入volatile变量并且线程B随后读取相同的volatile变量,线程A在写入之前的所有volatile变量,当线程B读取volatile变量后也将对线程B可见。

如果线程A读取volatile变量,则读取变量时线程A可见的所有变量volatile也将从主存储器重新读取。
让我用代码示例说明:

public class MyClass {
    private int years;
    private int months
    private volatile int days;


    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

该udpate()方法写入三个变量,其中只有days 是volatile。

完全volatile可见性保证意味着,当写入days值时,线程可见的所有变量也会写入主存储器。这意味着,当days的值被写入主内存,years和months的值也被写入主存储器。

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

public class MyClass {
    private int years;
    private int months
    private volatile int days;

    public int totalDays() {
        int total = this.days;
        total += months * 30;
        total += years * 365;
        return total;
    }

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

注意totalDays()通过读取的值的方法开始days到 total变量。当读取的值days,值months 和years也被读入到主存储器中。因此可以保证看到的最新值days,months并years与上述读取序列。

指令重新排序挑战

只要指令的语义含义保持不变,Java 虚拟机和CPU就可以出于性能原因重新排序程序中的指令。例如,请查看以下说明:

int a = 1;
int b = 2;

a++;
b++;

这些指令可以按以下顺序重新排序,而不会丢失程序的语义含义:`

int a = 1;
a++;

int b = 2;
b++;

然而,当其中一个变量是volatile变量时,指令重新排序时将面临挑战。让我们看看MyClass这个Java volatile教程前面的例子中的类:

public class MyClass {
    private int years;
    private int months
    private volatile int days;


    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

一旦调用update()方法写入一个值到days, 新写入到years 和months的值同样被写入到主内存。
但是,如果Java VM重新排序指令,如下所示:

public void update(int years, int months, int days){
    this.days   = days;
    this.months = months;
    this.years  = years;
}

当我们去修改days变量时months的值和years值仍写入到主内存,但这次发生的是days修改是发生在写months years之前。新的值(months,years)不能正确被其他线程可见.重新排序的指令的语义含义已经改变。
Java有一个解决这个问题的方法,我们将在下一节中看到。

3.发生在保证之前

从Java 5开始,volatile关键字还提供了额外的功能,可确保包括非易失性变量在内的所有变量的值与Volatile写操作一起写入主存储器。

这称为Happens-Before,因为它为所有变量提供了对另一个读取线程的可见性。此外,JVM不会重新排序volatile变量的读写指令。

Java volatile Happens-Before Guarantee

为了解决指令重新排序挑战,volatile除了可见性保证之外,Java 关键字还提供“先发生”保证。事先发生的保证保证:
1.如果读取/写入最初发生在对volatile变量写入之前,则无法重新排序对其他变量的读和写操作。

对volatile变量的写入之前的读/写将会保证在写入”volatile"之前“先发生”。
请注意,在写入“volatile"变量之前,可能会对其他变量的读/写进行重新排序,以使其在写入“volatile"后发生。只是不是另一种方式。从以后到以前是允许的, 但从以前到以后是不允许的。

2.如果读取/写入最初发生在对volatile变量读取之后,则无法重新排序对其他变量的读和写操作。

请注意, 在读取“volatile"变量之前, 可能会对其他变量的读取进行重新排序, 以使其在读取“volatile"后发生。只是不是另一种方式。从之前到以后是允许的, 但从以后到以前是不允许的。

上述情况-在保证确保volatile关键字的可见性保证被强制执行之前。

volatile is Not Always Enough

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

两个线程已将共享计数器变量读入其本地CPU高速缓存并递增。

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

什么时候挥发够了?

正如我前面提到的,如果两个线程都在读取和写入共享变量,那么使用 volatile关键字是不够的。 在这种情况下,您需要使用synchronized来保证变量的读取和写入是原子的。读取或写入volatile变量不会阻止线程读取或写入。为此,您必须synchronized 在关键部分代码

作为synchronized块的替代方法,您还可以使用java.util.concurrent包中提供的原子数据类型。例如,AtomicLong或者 AtomicReference来避免竞争条件。

  • 标记为synchronized的逻辑变为同步块,在任何给定时间只允许一个线程执行。

如果只有一个线程读取和写入volatile变量的值,而其他线程只读取变量,那么读取线程将保证看到写入volatile变量的最新值。如果不使变量变为volatile,则无法保证。

volatile关键字保证适用于32位和64位变量。

挥发性的性能考虑因素

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

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

推荐阅读更多精彩内容