volatile关键字的作用

volatile关键字的作用

1.java内存模型

如上图所示,所有线程的共享变量都存储在主内存中,每个线程都有一个独立的工作内存,每个线程不直接操作在主内存中的变量,而是将主内存上变量的副本放进自己的工作内存中,只操作工作内存中的数据。当修改完毕后,再把修改后的结果放回到主内存中。每个线程都只操作自己工作内存中的变量,无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

2.内存中的交互操作有很多,和volatile有关的操作为
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入):作用于工作内存的变量,他把read操作从主内存得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,他把一个从执行引擎接受到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节指令码指令时执行这个操作。
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入):作用于主内存的变量,他把store操作从工作内存中一个变量的值传送到主内存的变量中。

对被volatile修饰的变量进行操作时,需要满足以下规则:
1)线程对变量执行前的一个动作是load时才能执行use,反之只有后一个动作是use时才能执行load。线程对变量的read,load,use动作关联,必须连续一起出现。这保证了线程每次使用变量时都需要从主内存拿到最新的值,保证了其他线程修改的变量本线程能看到。
2)线程对变量执行的前一个动作是assign时才能执行store,反之只有后一个动作是store时才能执行assign。线程对变量的assign,store,write动作关联,必须连续一起出现。这保证了线程每次修改变量后都会立即同步回主内存,保证了本线程修改的变量其他线程能看到。
3)假设有线程T,变量X,变量Y。假设动作A是T对X的use和assign动作,B是与A关联的read或write动作,动作C是T对Y的use和assign动作,动作D是与C关联的read或write动作。加入A先于C,那么B先于D
。这保证了volatile修饰的变量不会被指令重排序优化,代码的执行顺序与程序的顺序相同。

3.MESL缓存一致性协议
volatile可见性是通过汇编加上Lock前缀指令,触发底层的MESL缓存一致性协议来实现的。当然这个协议有很多种,不过最常用的就是MESL。MESL表示四种状态:
M 修改(Modifled):此时缓存行中的数据与主内存中的数据不一致,数据只存在于本工作内存中。其他线程从主内存中读取共享变量值的操作会被延迟执行,直到该缓存行将数据写回到主内存中。
E 独享(Exclusive):此时缓存行中的数据与主内存中的数据一致,数据只存在于本工作内存中。此时会监听其他线程读主内存中共享变量的操作。如果发生,该缓存行需要变成共享状态。
S 共享(Shared):此时缓存行中的数据与主内存中的数据一致,数据存在于很多工作内存中。此时会监听其他线程使缓存行无效的请求,如果发生,该缓存行需要变成无效状态。
I 无效(invalid):此时该缓存行无效。

假如说当前有一个cpu去主内存拿到一个变量x的值初始为1,放到自己的工作内存中。此时他的状态就是独享状态E,然后此时另一个cpu也拿到了这个x的值,放到自己的工作内存中。此时之前的那个cpu会不断地监听内存总线,发现这个x有多个cpu在获取,那么这个时候这两个cpu所获得的x的值的状态就都是共享状态S。然后第一个cpu将自己工作内存中的x的值带入到自己的ALU计算单元去进行计算,返回时x的值变为2,接着会告诉内存总线,此时自己的x的状态置为修修改状态M。而另一个cpu此时会不断地监听内存总线,发现这个x已经有别的cpu将其置为了修改状态,所以自己内部的x的状态会被置为无效状态I,等待第一个cpu将修改后的值刷回主内存后,重新去获取新的值。这个谁先改变的x的值可能是同一时刻进行修改的,此时cpu会通过底层硬件在同一个指令周期内进行裁决,裁决是谁进行修改的,就置为修改状态,而另一个就置为无效状态,被丢弃或者被覆盖。

当然MESL也会有失效的时候,缓存的最小单元是缓存行,如果当前的共享数据的长度超过一个缓存行的长度的时候,就会使MESL协议失败,此时的话就会触发总线加锁的机制,第一个线程cpu拿到这个x的时候,其他线程都不允许去获取这个x的值。

4.禁止指令重排序优化
1)指令重排序基本概念:
指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能的提高并行度。指令重排序包括编译器重排序和运行时重排序。

2)如果一个操作不是原子的,就会给JVM留下重排序的机会。下面我们举一个非常经典的例子。

public class SingletonDemo {    private static SingletonDemo instance=null;    private SingletonDemo() {    }    public static SingletonDemo getInstance(){        if(null==instance){            synchronized (SingletonDemo.class){                if(null==instance){                    //非原子操作                    instance=new SingletonDemo();                }            }        }        return instance;    }}

这是单例模式中的"双重检查加锁模式",由于instance=new SingletonDemo();并不是一个原子操作,其实际可以抽象为下面几条JVM指令。

//分配对象的内存空间1.memory=allocate();//初始化对象2.ctorInstance(memory);//设置instance指向刚分配的内存地址3.instance=memory;

上面的操作2依赖于操作1,但是操作3并不依赖于操作2.所以JVM可以针对他们进行指令的优化重排序,经过重排序后如下。

//分配对象的内存空间1.memory=allocate();//设置instance指向刚分配的内存地址2.instance=memory;//初始化对象3.ctorInstance(memory);

指令重排序之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。在线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值给instance引用,恰好另一个线程进入方法判断instance引用不为null,将其返回使用,导致出错。

3)对此我们可以用volatile关键字修饰instance变量,使得instance在读,写操作前后都会插入内存屏障,避免重排序。

    private volatile static SingletonDemo instance=null;

4)内存屏障。
volatile有序性是通过内存屏障实现的。JVM和cpu都会对指令做重排优化,所以在指令间插入一个屏障点,就相当于告诉JVM和cpu不能进行重排优化。具体分为读读,读写,写读,写写这四种屏障,同时他也会有一些插入屏障点的策略:
i)每个volatile写的前面插入一个store-store屏障,禁止上面的普通写和下面的volatile写重新排序。
ii)每个volatile写的后面插入一个store-load屏障,禁止上面的volatile写和下面的volatile读/写重新排序。
iii)每个volatile读的后面插入一个load -load屏障,禁止下面的普通读和上面的volatile读重新排序。
iv)每个volatile读的后面插入一个load -store屏障,禁止下面的普通写和上面的volatile读重新排序。
上面的插入策略非常保守,但是他可以保证在任意处理器平台上的正确性,在实际执行时,编译器可以省略没必要的屏障点,同时在某些处理器上会做进一步的优化。

5.不保证原子性
尽管volatile关键字可以保证内存可见性和有序性,但不能保证原子性。也就是说,对volatile修饰的变量进行操作,不保证多线程安全。下面我们举个例子:

public class AtomicityDemo extends Thread {    private static volatile int increase = 0;    //原子类(实现了原子性)作为对照组    private static AtomicInteger aInteger = new AtomicInteger();    private static void increaseFun() {        increase  ;        //计算多线程调用次数,每次调用会 1,返回int类型数据。        aInteger.incrementAndGet();    }    @Override    public void run() {        int i = 0;        while (i < 10000) {            increaseFun();            i  ;        }    }    public static void main(String[] args) {        AtomicityDemo ad = new AtomicityDemo();        int thread = 10;        Thread[] threads = new Thread[thread];        for (int i = 0; i < thread; i  ) {            threads[i] = new Thread(ad, "线程"   i);            threads[i].start();        }        while (Thread.activeCount() > 2) {            Thread.yield();        }        System.out.println("volatile的值为:"   increase);        System.out.println("AtomicInteger的值为:"   aInteger);    }}

输出:
volatile的值为:98490
AtomicInteger的值为:100000

上面的代码我们采用了10个线程同时对volatile修饰的变量进行自增1000的操作。如果volatile变量是并发安全的话,运行结果应该为10000,可结果很显然不是,由此可见volatile修饰的变量并不保证原子性。
increase 这行代码不是原子操作, 操作的执行过程如下所示:
1.首先获取变量increase的值。
2.将该变量的值 1。
3.将该变量的值写回到对应的主内存中。
虽然每次获取increase值的时候,都拿到的是主内存中的最新变量值,但是在进行第二部 1的操作的时候,可能其他线程在此期间已经对increase进行了 1,这时候就会触发MESL协议的失效动作,将该线程内的值无效,那么 1的操作就失效了。所有会产生多个线程同时做了 1的操作,但是实际结果只加了1次,这就造成了返回结果小。对此我们可以用以下方式解决这个问题:

 private synchronized static void increaseFun() {        increase  ;        //计算多线程调用次数,每次调用会 1,返回int类型数据。        aInteger.incrementAndGet();    }

整理借鉴了很多大佬写的,在此无法一一说明,这只是个人用来查漏补缺的文章,如果对你有帮助我很高兴。

来源:https://www.icode9.com/content-4-853251.html

(0)

相关推荐

  • 分布式并发编程,线程安全性,原理分析

    初步认识 Volatile 一段代码引发的思考 下面这段代码,演示了一个使用 volatile 以及没使用volatile这个关键字,对于变量更新的影响 public class VolatileDe ...

  • 到底什么是内存可见性?

    我们都知道,volatile保证了内存可见性和禁止指令重排,但是对于内存可见性这一条,我一直没有完全弄明白,今天咱们一起看一下,这个可见性,到底是如何可见,数据到底是如何可见的. 首先我们要达成一个共 ...

  • 你还不懂可见性、有序性和原子性?

    前言 今天开始,王子准备开始一个新的专栏:并发编程专栏. 并发编程无论在哪门语言里,都属于高级篇,面试中也尝尝会被问到.想要深入理解并发编程机制确实不是一件容易的事,因为它涉及到计算机底层和操作系统的 ...

  • C语言中volatile关键字的作用

    一.前言 编译器优化介绍: 由于内存访问速度远不及CPU处理速度,为提高机器整体性能, 1)在硬件上:  引入硬件高速缓存Cache,加速对内存的访问.另外在现代CPU中指令的执行并不一定严格按照顺序 ...

  • C语言丨深入理解volatile关键字

    本篇文章是对C语言中关键字volatile的含义进行了详细的分析介绍,希望能在学习上帮助大家.   volatile是一个类型修饰符(type specifier).它是被设计用来修饰被不同线程访问和 ...

  • C/C++ 中 volatile 关键字详解 | 菜鸟教程

    C/C++ 中 volatile 关键字详解 | 菜鸟教程

  • volatile关键字的理解

    首先volatile只能修饰实例变量或者类变量,不能修饰方法.局部变量.方法参数等.并发的三个至关重要的特性,原子性.可见性.有序性,volatile只能保证前面的两个特性,所以使用volatile关 ...

  • volatile关键字详解

    volatile的三个特点 保证线程之间的可见性 禁止指令重排 不保证原子性 可见性 概念 可见性是多线程场景中才讨论的,它表示多线程环境中,当一个线程修改了共享变量的值,其他线程能够知道这个修改. ...

  • C++ 中 explicit关键字的作用

    explicit作用: 在C++中,explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换. explicit使用注意事项: exp ...

  • const关键字及其作用(用法),C语言const详解

    const 在实际编程中用得并不多,const 是 constant 的缩写,意思是"恒定不变的"!它是定义只读变量的关键字,或者说 const 是定义常变量的关键字. 说 con ...

  • java中final关键字的作用

    final关键字可以用于三个地方.用于修饰类.类属性和类方法. 被final关键字修饰的类不能被继承,被final关键字修饰的类属性和类方法不能被覆盖(重写): 对于被final关键字修饰的类属性而言 ...

  • Java中的static关键字和new关键字作用介绍

    一.static关键字的作用 1.可以用于修改类的成员变量.代码块和类 通过static可以将类的成员声明为静态成员,静态的成员归属于整个类,而不是属于某个对象.无论通过类还是对象访问静态成员,操作的 ...