volatile关键字详解

一、volatile关键字简介

synchronized关键字是阻塞式同步,在线程竞争激烈的时候会逐渐由偏向锁膨胀为重量级锁。而volatile是JVM提供的最轻量级的同步机制。JMM告诉我们各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。不过线程在工作内存中进行操作后将会何时写入主内存中?这个时机普通机制是没有规定的。

volatile一般用于修饰会被不同线程访问和修改的变量,而针对volatile修饰的变量给JVM给了规定:线程对volatile变量的修改会立刻被其他线程感知,即被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,这样就不会出现数据脏读的现象,保证了数据的可见性

volatile具有可见性和有序性

二、volatile实现原理

加入volatile关键字的代码的class字节码中会多出了一个lock前缀指令,lock指令相当于一个内存屏障,主要做了三件事:

  1. 重排序时不能把后面的指令重排序到内存屏障之前的位置
  2. 将当前处理器缓存行的数据写回系统内存
  3. 这个写回内存的操作会使其他CPU里缓存的该内存地址的数据无效,即新写入的值对别的线程可见

经过这一波操作后,其他的线程发现自己工作内存中的缓存失效后,就会从内存中重新读取该变量数据,即保证了其他线程可以获取当前最新值。即可以说volatile实现了缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己的缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

三、volatile的happens-before关系和内存语义分析

在之前JMM一文中对happens-before规则介绍,有一条是:对一个volatile域的写,happens-before于任意后续对这个volatile域的读

来看一段代码

class VolatileExam{
    private int a = 0;
    private volatile boolean flag = false;

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

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

对volatile的happens-before分析:

  • 线程1先执行writer方法,然后线程2执行reader方法。
  • 我们由happens-before规则推知,2 happens-before 3 (volatile变量的写happens-before于任意后续对volatile变量的读)
  • 由传递性可以得知1 happens-before 4
  • 由happens-before规则:如果A happens-before B,则A的执行结果对B可见,且A的执行顺序先于B的执行顺序
  • 那么2的执行结果对3可见,也就是说线程1将flag修改为true,线程2能够迅速感知

volatile的内存语义分析:

  • 如果线程1先进行writer方法,随后线程2进行reader方法。一开始的本地都是a和flag的初始化状态
  • 在线程1线程2的本地内存里,线程1对初始值进行了修改并写入主内存中,而在线程2的本地内存里还是原来的值
  • 由于volatile变量写后,线程中本地内存中共享变量就会置为失效状态,因此线程2需要再次从主内存中读取最新的共享变量值。从横向看,线程1和线程2进行了通信,线程1在写volatile变量的时候告诉线程2:你的本地内存中的值是旧的
  • 线程2在读取volatile变量的时候就被告知目前自己的本地值是旧的,那线程2就只能去主内存中去取最新值了

四、volatile内存语义的具体实现

为了性能优化,JMM在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序,那如果想阻止重排序就得添加内存屏障。


在这里插入图片描述

四个内存屏障:

屏障类型 指令类型 说明
LoadLoadBarriers Load1;LoadLoad;Load2 确保Load1的数据的装载先于Load2及所有后续装载指令的装载
StoreStoreBarriers Store1;StoreStore;Store2 确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储
LoadStoreBarriers Load1;LoadStore;Store2 确保Load1的数据的装载先于Store2及所有后续存储指令的存储
StoreLoadBarriers Store1;StoreLoad;Load2 确保Store1的数据对其他处理器可见(刷新到内存)先于Load2及所有后续的装载指令的装载

为了实现volatile内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序:

  1. 在每个volatile写操作的前面插入一个StoreStore屏障
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障
  4. 在每个volatile读操作的后面插入一个LoadStore屏障

StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;
StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序
LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序

五、volatile如何保证内存可见性

复习一下JMM的8种原子操作:

  1. lock(锁定):作用于主内存中的变量,把一个变量表示为一个线程独占的状态
  2. unlock(解锁):作用于主内存中的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  3. read(读取):作用于主内存的变量,把一个变量的值从主内存读取到线程的工作内存中,以便于后面的load操作
  4. load(载入):作用于工作内存中的变量,把read操作从主存中得到的变量值放入工作内存中的变量副本
  5. use(使用):作用于工作内存中的变量,把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
  6. assign(赋值):作用于工作内存中的变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就执行这个操作
  7. store(存储):作用于工作内存中的变量,把工作内存中一个变量的值传送给主存中以便于后面的write操作
  8. write(写入):作用于主内存中的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中

volatile规定:read、load、use动作必须连续出现assign、store、write动作必须连续出现
所以volatile保证:每次读取前必须先从主内存刷新最新的值,每次写入后必须立即同步回主内存当中。即volatile关键字修饰的变量看到的随时是自己的最新值。

六、volatile的注意事项

public class VolatileExample {

    private static volatile int counter = 0;

    public static void main(String[] args) {
        //开十个线程,让他们每个都自增10000次,理论上应该得到10000
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++)
                        counter++;
                }
            });
            thread.start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}

多次运行看出,每次都得不到10000,这说明volatile并不能保证整体原子性。问题就是counter++并不是一个原子操作,他包含了三个步骤:

  1. 读取counter的值
  2. 对counter+1
  3. 将新的值赋给变量counter

这么看来如果线程1读取counter到工作内存后,其他线程对这个值已经做了自增操作,那么线程A的这个值自然就是一个过期的值,造成了数据的脏读,因此结果必然小于10000。

如果想让volatile保证整体原子性,必须符合:

  1. 运算结果不依赖变量的当前值,或者能够确保只有一个线程修改变量的值;
  2. 变量不需要与其他的状态变量共同参与不变约束

如果编译器经过分析后,认定一个volatile变量只会被单个线程访问,那么编译器可以把这个volatile变量当做一个普通的变量来对待。

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