Android线程篇(九):关键字Volatile

九旬老太为何惨死街头 数百头母驴为何半夜惨叫 小卖部安全套为何屡遭黑手 女生宿舍内裤为何频频失窃 连环强奸母猪案究竟是何人所为 老尼姑的门夜夜被敲究竟是人是鬼 数百头母狗意外身亡背后又隐藏着什么 这一切的背后!!到底是人性的扭曲还是道德的沦丧?下面带大家走进Java关键字之——volatile!

在多线程编程中,我们最常用的是synchronized,而对volatile的使用,却对volatile的使用较少。这一方面是因为volatile的使用场景限制,另一方面是因为volatile使用需要更高的技术水平。

volatile关键字好多人都听过,或许也使用过,从字面意思来看很好理解,但是要用好真的不是一件容易的事情。笔者在之前也看过好多大佬讲过volatile,但是仍然没有弄明白。自己在学习的过程中看了许多大神的博客,他们在讲解Java内存模型的时候总是把CPU的内存架构和Java内存模型混为一谈,事实上这俩个完全不是一个概念,CPU的高速缓存并不是主存的一部分,而是CPU本身自带的,就像CPU里面的寄存器一样。或许,他们都有自己的理解!在讲解Volatile关键字之前,必须了解Java虚拟机的内存模型和CUP的内存架构,不了解的同学速度学习前几篇。

Java内存模型:
Android线程篇(五):Java内存模型
CPU内存架构:
Android线程篇(六):CPU内存架构
多线程下的缓存一致性问题:
Android线程篇(七):多线程下的缓存一致性问题
原子操作和指令重:
Android线程篇(八):原子操作和指令重排

volatile翻译过来意思是:不稳定的,易挥发的;

有道是:太极生两仪,两仪生四象,四象生八卦,天地造就万物生灵,既然它存在,必然有它存在的意义,volatile到底有什么作用,意义何在?

部分摘自“海 子”博客,感谢作者:http://www.cnblogs.com/dolphin0520/p/3920373.html

1.可见性

Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

2.有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

下面就来具体介绍下happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁unLock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
    这8条原则摘自《深入理解Java虚拟机》。

这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

下面我们来解释一下前4条规则:

对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

第四条规则实际上就是体现happens-before原则具备传递性。

学习了volatile的作用之后,我们继续上上篇文章的例子来看:

    public int count = 0;
    public int TestVolatile(){
        final CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                    }

                    count++;
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("<<<<<"+count);
        return count;
    }

volatile具有可见性,给count加上volatile就线程安全了,输出的结果就是我们所期望的结果,事实真的如此吗?

public volatile int count = 0;
    public  int TestVolatile() {
        final CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                    }

                    increase();
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("<<<<<" + count);
        return count;
    }
    public void increase() {
        count++;

    }

变量count被使用了volatile修饰,那么在thread1中,当count变为3的时候,就会强制刷新到主存。如果这个时候,thread2已经将count =2从从主存映射到缓存上,那么在对count进行自增操作以前,会重新到主存中读取count =3,然后自增到count =4,然后写回到主存。上面的过程很完美,但这样是否保证了count最终的结果一定是4呢?

当然,结果要让大家失望了。

我们来分析下为什么?
来看看count++操作的时候内存都做了什么操作:
1.从主内存里面(栈)读取到count的值,到CPU的高速缓存当中
2.寄存器对count的值进行加一操作
3.将CPU的高速缓存当中的count值刷新到主内存(栈)当中

我们可以看到++这个操作非原子,先读count,然后+1, 最后再写 count

如果变量count被使用了volatile修饰,那么在thread1中,当count变为3的时候,就会强制刷新到主存。如果这个时候,thread2已经将count =2从从主存映射到缓存上并且已经做完了自增操作,此时count =3,那么最终主存中count值为3。

所以,如果我们想让count的最终值是4,仅仅保证可见性是不够的,还得保证原子性。也就是对于变量count的自增操作加锁,保证任意一个时刻只有一个线程对count进行自增操作。可以说volatile是一种“轻量级的锁”,它能保证锁的可见性,但不能保证锁的原子性。
具体解决办法请移步:
多线程下的缓存一致性问题:
Android线程篇(七):多线程下的缓存一致性问题

继续来一个例子:

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
//线程2
stop = true;

这段代码很典型,很多人都会采用这种标记办法来处理是否进入循环。但是事实上,这段代码会完全运行正确么?不一定,也许在大多数时候是正确的,但是也有可能是错误的(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

下面解释一下这段代码为何会有问题。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

但是用volatile修饰stop之后就变得不一样了:

//线程1
volatile boolean stop = false;
while(!stop){
    doSomething();
}
//线程2
stop = true;

第一:使用volatile关键字会强制将修改的值立即写入主存;

第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效,也就是执行线程1的CPU缓存中的stop无效。

第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

那么线程1读取到的就是最新的正确的值。

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

推荐阅读更多精彩内容