浅析Volatile关键字

浅析Volatile关键字

在java中线程并发中,线程之间通信方式分为两种:共享内存和消息传递。共享内存指的是多个线程之间共享内存的属性状态;消息传递指的是线程之间发送信息来通信。在介绍volatile,我们先了解一下共享内存一些基本概念。

JMM

Java内存模型(简称JMM)控制线程通信,可以分为主内存和本地内存,每个线程拥有一个本地内存。

 

如图,一般主存只有一个,根据线程数不同,本地内存(又称工作内存)数目也不同,本地内存是抽象出来的概念,实际上不存在。

线程之间通过内存来同步状态可以分为以下几个步骤:

1、  线程1从主存获取变量x=1,假设此时其他线程x都为1

 

2、  线程1将本地内存中x值修改为2

3、  线程1将本地内存中x值放回主存

4、  其他线程同步主存数据

以上,就是理想状态线程之间通过主存通信状态,保证了内存的可见性

 

指令重排序

         java程序在运行时,并不会严格按照程序代码编写的顺序执行代码,会对其进行重排序。例如以下例子:

 

         在程序执行代码的时候,步骤1和步骤2可能被重排序,导致2可能在1之前先执行,但是重排序的前提是在单线程情况下不会改变运行结果,因此1和2可以重排序,但是3引用了1和2中的变量,因此3不会被重排序到1或者2之前。在单线程情况下,重排序不会影响,但在多线程中会导致结果不可预见,可以使用其他方法来保证有序性

原子性操作

         前面提到了可见性有序性原子性是在并发编程中最常遇见的三个概念。原子性指不可分割的操作。举例说明,在32为JVM中长度小于32为的数据读写都是原子性操作,但是64位数据结构的读写不是原子性操作。例子如下:

1、  线程1和2中有共同变量x,为Long型

2、  线程1将x改为LONG_MAX/2

3、线程1在修改本地内存中的值到主存时,先将Long的前32保存到主存中,此时主存中的x值为LONG_MAX/2的前32为加上LONG_MAX后32为,我们设为z

 

4、线程2读取主存中的x值并更新

5、线程1同步后32位数据到主存中,之后线程3也同步主存数据

         最后可以看到,由于64为数据读写不是原子性的,单只线程2获取的数据为错误数据,如果,线程1同步主存的操作为原子性,那么就不会出现以上情况。

 

Volatile关键字

Volatile能够保证程序的原子性,例如将Long或者Double设置为volatile可以避免由于非原子性读写造成的数据不同步。但是,类似于x = x+1;这种类型的复合操作,volatile依旧服务保证原子性。

1、  volatile关键字x,线程1将x修改为LONG_MAX/2,主存为z

2、此时,由于x是volatile,因此线程要从主存读取这个值的时候,并不会主动到主存中获取,而是等到其他线程通知他再去获取。因此,线程1将完整的x值写到主存中。

3、之后,通知其他线程去获取x的值,以实现数值同步,保证原子性

         由于volatile的值会及时将值刷新到主存中,volatile也保证了可见性

         那么volatile是否能保证有序性?个人见解是能在一定程度上保证数据的可见性,但是没有了解到有资料明确说明volatile保证了原子性。

         volatile实际上设置了内存屏障,由于内存屏障的存在,保证了多线程下不会由于重排序导致数据不一致的问题。

是否重排序

第二个操作

第一个操作

普通读写

Volatile读

Volatile写

普通读写

 

 

No

Volatile读

No

No

No

Volatile写

 

No

No

可知volatile写之前的操作不会被重排序到之后;volatile读之后的操作不会被重排序到之前;第一个操作是volatile写,第二个操作是volatile读不会重排序。

         JMM采取的内存屏障策略为:

1、  在volatile写前面插入StoreStore屏障

2、  在volatile写后面插入StoreLoad屏障

3、  在volatile读后面插入LoadLoad屏障

4、  在volatile读后面插入LoadStore屏障

 

StoreStore屏障保证volatile写之前的普通写刷新到主存当中,避免与前面普通写重排序;

StoreLoad屏障保证volatile写不会和之后的volatile读写重排序;

LoadLoad屏障禁止volatile读不会与下面的普通读重排序;

LoadStore屏障禁止volatile读不会与下面的普通写重排序。

具体可见p1,p2

 

P1

 

P2

 

一个例子

         如下例子,线程1执行writer方法,线程2执行reader方法,由于JVM会进行重排序,因此可能的执行顺序为:

         1、线程1执行步骤2,此时a的值还是0

         2、线程2执行步骤3,因为重排序不会在引起单线程结果改变,因此不会先执行4

         3、线程2执行步骤4,i的值为0

         4、线程1执行步骤1

 

Int a = 0
Boolean flag =false
public void writer(){
  a = 1;        //1
  flag = true;   //2
}
public void reader(){
  if(flag){      //3
     int i = a;  //4
  }
}

可见,由于重排序导致结果改变,修改代码如下:

Int a = 0
volatile boolean flag =false
public void writer(){
    a = 1;        //1
    flag = true;   //2
}
public void reader(){
    if(flag){      //3
    int i = a;  //4
    }
}

执行步骤如下:

1、  线程1执行1,因为volatile变量flag前面插入StoreStore屏障,不会重排序

2、  线程1执行2;

3、  线程2执行3;

4、  线程2执行4,i为2;

结果符合预期。

posted @ 2019-09-07 10:42  玩过熊  阅读(217)  评论(0编辑  收藏  举报