若泽大数据 www.ruozedata.com

ruozedata


  • 主页

  • 归档

  • 分类

  • 标签

  • 发展历史

  • Suche

你真的了解volatile关键字吗?

Veröffentlicht am 2019-06-05 | Bearbeitet am 2019-06-20 | in Java | Aufrufe:

volatile的语义

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性

    即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的

  • 禁止进行指令重排序

    举例,线程1先执行,线程2后执行:

    1
    2
    3
    4
    5
    6
    7
    1//线程1
    2boolean stop = false;
    3while(!stop){
    4 doSomething();
    5}
    6//线程2
    7stop = true;

这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法

但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?

不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)

无法中断,导致死循环的原因:

  • 每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中
  • 那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去

当我们使用volatile修饰了flag之后就不一样了,使用volatile关键字会强制将修改的值立即写入主存:

  • 使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存值无效
  • 由于线程1的工作内存中缓存变量stop的缓存值无效,所以线程1再次读取变量stop的值时会去主存读取stop的值
  • 那么在线程2修改stop值时(这里包括2个操作:修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存值无效,然后线程1读取时,发现自己的缓存值无效,它就会等待缓存值对应的主存地址被更新之后,然后去对应的主存读取最新的值
  • 那么,线程1读取到的就是最新的正确的值

volatile与原子性(无法保证所有的操作都具有原子性)

从上面知道volatile关键保证了操作的可见性,但是volatile能保证对变量操作的原子性吗?

看如下的一个例子

有个被volatile修饰的int类型的变量inc初始值为0,此时有10个线程对这个变量去进行增加的操作,每个变量增加到1000,那么最终结果按道理来说是1000*10=10000的,但是并不能,运行出来可能是一个小于10000的数字(调用了,Thread.yield()方法,暂停当前正在执行的线程对象,并执行其他线程)

存在的误区

可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000

事实

在前面已经提到过,自增操作是不具备原子性的,它的步骤包括:

  • 读取变量的原始值
  • 进行加1操作
  • 写入工作内存,刷到主存中去

那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

  • 假如某个时刻变量inc的值为10,此时线程1对变量进行自增操作:线程1先读取了变量inc的原始值,然后线程1被阻塞了
  • 然后线程2对变量进行自增操作:线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存值无效,同时线程2从主存中读取到的值也是没有任何修改的10
  • 因此线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存
  • 然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
  • 那么两个线程分别进行了一次自增操作后,inc却只增加1

存在的疑问

解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存值无效吗?然后其他线程去读就会读到新的值,对,这个没错。

这个就是上面的happens-before规则中的volatile变量规则,但是要注意:

  • 线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改
  • 然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值

    根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的

解决方案

解决的方法也就是提供原子性的自增操作即可:

在Java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增,自减、以及加法操作,减法操作进行了封装,保证这些操作是原子性操作;atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作

针对本案例,可以使用AtomicInteger来替换int,它利用了CAS算法来保证了原子性

volatile与有序性(防止指令重排)

在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性;volatile关键字禁止指令重排序有两层意思:

  • 当程序执行到volatile变量的读操作或者写操作时

    在volatile这个操作前面的操作的更改肯定全部已经进行

    且结果已经对后面的操作可见

  • 在volatile这个操作后面的操作肯定还没有进行

    在进行指令优化时

    不能将在对volatile变量访问的语句放在其后面执行

    也不能把volatile变量后面的语句放到其前面执行

例子

可能上面说的比较绕,举个简单的例子:

1
2
3
4
5
6
7
1//x、y为非volatile变量
2//flag为volatile变量
3x = 2; //语句1
4y = 0; //语句2
5flag = true; //语句3
6x = 4; //语句4
7y = -1; //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候:

  • 不会将语句3放到语句1、语句2前面
  • 也不会将语句3放到语句4、语句5后面
  • 但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的

并且volatile关键字能保证,执行到语句3时:

  • 语句1和语句2必定是执行完毕了的
  • 且语句1和语句2的执行结果对语句3、语句4、语句5是可见的

指令重排的应用(双重懒加载的单例模式)

一个最经典的使用场景就是双重懒加载的单例模式了:

单例模式

这里的volatile关键字主要是为了防止指令重排

singleton = new Singleton()这段代码,其实是分三步走的:

  • 分配内存空间
  • 初始化对象
  • 将singleton对象指向分配的内存地址

    加上 volatile 是为了让以上的三步操作顺序执行,反之有可能第三步在第二步之前被执行,那么就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错

volatile的实现机制

前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的;下面这段话摘自《深入理解Java虚拟机》 :
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令:

  • lock前缀指令实际上相当于一个 内存屏障(也成内存栅栏),内存屏障会提供3个功能:
  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置

    也不会把前面的指令排到内存屏障的后面

    即在执行到内存屏障这句指令时,在它前面的操作已经全部完成

  • 它会强制将对缓存的修改操作立即写入主存

  • 如果是写操作(即修改操作),它会导致其他CPU中对应的缓存值无效
ruozedata WeChat Bezahlung
# java
捷报:连续4周若泽数据第16-19名学员喜捷offer(含面试题)
生产HDFS Block损坏恢复最佳实践(含思考题)
  • Inhaltsverzeichnis
  • Übersicht

ruozedata

若泽数据优秀博客汇总
155 Artikel
31 Kategorien
74 schlagwörter
RSS
GitHub B站学习视频 腾讯课堂学习视频 官网
  1. 1. volatile的语义
  2. 2. volatile与原子性(无法保证所有的操作都具有原子性)
    1. 2.1. 看如下的一个例子
    2. 2.2. 存在的误区
    3. 2.3. 事实
    4. 2.4. 存在的疑问
    5. 2.5. 解决方案
  3. 3. volatile与有序性(防止指令重排)
  4. 4. 指令重排的应用(双重懒加载的单例模式)
  5. 5. volatile的实现机制
|
若泽数据
|