volatile 的语义

文章会在我的独立博客同步更新

volatile 是 java 中一个非常常见,功能非常强大的一个关键字,大家用的最多的地方可能就是单例模式的双重检查锁的写法中。提到 volatile ,不得不提 synchronized , synchronized 是一个重量级锁,那么 volatile 是一个轻量级锁吗?并不是, volatile 是一个轻量级的同步关键字,那么 volatile 的语义到底是什么呢?这就是这篇文章要介绍的内容。

从单例模式的双重检查锁写法说起

首先我们看一下常见的单例模式的双重检查锁写法。更多单例模式的写法

public class SingleInstance {
    private static SingleInstance sSingleInstance;

    private SingleInstance() {
    }

    public static SingleInstance getInstance() {
        if (sSingleInstance == null) {
            synchronized (SingleInstance.class) {
                if (sSingleInstance == null) {
                    sSingleInstance = new SingleInstance();
                }
            }
        }
        return sSingleInstance;
    }
}

上面这种写法在 《Java 并发编程实战》 一书中的评价是【臭名昭著】 ,可见这种写法是有非常大的问题,不过幸运的是我们有 volatile 关键字,在 Java 5 以上将 sSingleInstance 用 volatile 关键字修饰就可以解决(具体问题我们下面再讨论)。 那么 volatile 为什么能解决问题, volatile 又为程序员保证了什么,这就是下面要讨论的问题。

可见性

在 JMM 中,为了提高程序性能,线程对于变量的读写不会直接作用于主存,而是会先作用于相对应的本地内存,最后会在合适的时机再同步到主存。而 volatile 的可见性是指,线程每次在更新本地内存的变量之后,会同步刷新到主存中去,同样的线程每次在读 volatile 变量时都会将本地内存中的值置为无效。然后线程会直接去主存中读取相应的值 。下面我们用一个例子再加深点认识。

//程序片段1
class VolatileExample{
    int a = 0;
    boolean flag = false;

    public void write(){
        a = 1;          //1
        flag = true;    //2
    }

    public void read(){
        if(flag){
            int i = a;  //3
            ...         //4
        }
    }

}

假设 write 方法在 线程 A 中执行, read 方法在线程 B 中执行,我们不考虑其他因素(重排序),假设 A 线程先执行, B 线程后执行,那么当 B 线程执行时, B 线程能否正确读取到 flag 和 a 的值呢?很可惜,答案是不一定。因为 JVM 不保证何时会将本地内存中的值同步到主存中去。如果我们将程序改成下面这样,结果又是如何呢?

//程序片段2
class VolatileExample{
    int a = 0;
    volatile boolean flag = false;

    public void write(){
        a = 1;          //1
        flag = true;    //2
    }

    public void read(){
        if(flag){
            int i = a;  //3
            ...         //4
        }
    }

}

同样的我们暂时不考虑重排序,将 flag 使用 volatile 修饰之后, volatile 可以保证在线程 A 修改了 falg 值之后会将 flag 的值同步到主存中去,同样的在 B 线程读取 flag 的时候也会去主存中读取。那么我们还有一个问题,这个时候虽然线程 B 可以正确读取到 flag 的值,那么线程 B 还能正确读取到 a 的值吗?答案是:可以。 volatile 会保证在同步 flag 的值到主存的同时会将写 volatile 变量之前的操作同时同步到主存中去。同样的当线程 B 开始去主存中读取 volatile 时,也会去主存中读取 a 的值。
JMM 的抽象示意图如下:

JMM-ABS.png

阻止重排序

重排序:编译器和处理器有可能会对不存在数据依赖的两条指令进行重排序,这里的数据依赖仅指单线程或单个处理器。还是以上面的程序片段1为例,重排序是指 1 和 2 以及 3 和 4 的执行顺序不可预测。可能的执行顺序有:1->2->3->4;2->1->3->4;1->2->4->3;2->1->4->3;
通过将程序片段1改为程序片段2就可以阻止重排序,最终的执行结果就是:1->2->3->4 ;
为了实现 volatile 内存语义, JMM 针对编译器制定的重排序规则如下:

reorder_rule.png

从上表我们可以看出:

  1. 当第二个操作是 volatile 写时,不管第一个操作是什么,都不会重排序
  2. 当第一个操作是 volatile 读时,不管第一个操作是什么,都不会重排序
  3. 当第一个操作是 volatile 写,第二个操作是 volatile 读是不会重排序

再探双重检查锁

下面我们再来看看为什么下面这种双重检查锁写法就不是【臭名昭著】的了呢?更多单例模式的写法

public class SingleInstance {
    private static volatile SingleInstance sSingleInstance;

    private SingleInstance() {
    }

    public static SingleInstance getInstance() {
        if (sSingleInstance == null) {
            synchronized (SingleInstance.class) {
                if (sSingleInstance == null) {
                    sSingleInstance = new SingleInstance();
                }
            }
        }
        return sSingleInstance;
    }
}

首先,我们要讨论的是开头的那种写法存在的问题。
其实 sSingleInstance = new SingleInstance(); 这句代码看起来只有一句,但是被编译成指令是3句,分别是

  1. 为 SingleInstance 分配内存空间
  2. 调用 SingleInstance 的构造函数,初始化成员变量
  3. 为 sSingleInstance 赋值

根据前面的知识我们知道第 2 步和第 3 步可能会重排序,这样的话有可能某个线程获取到的单例就是未完全初始化的实例,为了解决这个问题,我们用 volatile 修饰 sSingleInstance 之后,根据上面的阻止重排序规则我们知道 volatile 写和前面的一条指令不会进行重排序,所以也就不会有问题了,这就是 volatile 的妙用。其实 volatile 远不止这点用处,在 Java 提供的并发包中有很多工具类的实现基础就是 volatile ,这些大家可以进一步了解。

参考文献

Java Memory Model
Java Volatile Keyword

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

推荐阅读更多精彩内容