Volatile并发理解

来源:互联网 发布:搜索引擎优化wtg168 编辑:程序博客网 时间:2024/06/11 17:33

Volatile作用

在java中主要用来修饰成员变量和类变量。其中,使用volatile修饰的变量在多线程环境中对所有多线程都是可见的。即,其中一个线程修改了volatile修饰的变量值,则其他线程能够立即得到最新修改的值。对于这个方面的理解,可以从并发编程模型(CPU,缓存,内存)和JAVA内存模型两个方面分析。

并发编程模型

现在的服务器都是多核的,而应用的都是发生在多线程环境中,那么在多核服务器中,多个处理器之间是如何与同一块内存中的数据进行交互呢?这就要先了解一下硬件层面的结构模型:
这里写图片描述
通过上述图中,可以发现在一个多核的服务器中,每一个处理器都有一块单独属于自己的缓存空间。每个线程将内存中的数据读取到高速缓存中进行运算操作,等待运算完再降变量值重写入内存中。
当多个cpu并发处理同一个变量时,是如何做到缓存之间变量值的同步呢?这就要求多个处理器之间遵守缓存协议(引用他人文章解释):
最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
这里写图片描述

java内存模型

java内存模型跟上面OS并发编程模型相似,也是分为主内存,工作内存,线程。其中,每个线程都有自己的工作内存。对于共享变量i,每个线程操作时都会将其拷贝一份到工作内存,每个线程操作的都是工作内存中的变量,禁止直接操作主内存中的数据。内存模型如下:
这里写图片描述
根据内存模型,可以看出对于主内存中的共享变量,每个线程都会有一个关于该共享变量的副本拷贝在其工作内存区。线程修改共享变量的值只能发生在工作内存区,等修改完毕后,会将最新值rewrite到主内存中。
对于单线程操作肯定是没有问题的,但是对多线程的操作,程序的执行结果就很容易发生错误。当然,对于多线程,在java中可以有很多种方式避免,比如同步(synchronized,lock,volatile)都可以做到同步。关于java内存模型可以看看我另外一篇分享的文章

volatile深探

看完上诉并发编程模型和java内存模型简单分析之后,应该大致对并发编程处理机制有个初步的了解。volatile在修饰变量(成员变量,类变量)到底起到什么作用,可以通过三个方面进行分析:

原子性

原子操作对于并发编程来说无疑是最重要的操作。原子操作指示要么全部成功要么全部失败。volatile修饰的变量对于其他线程是可见的,但是变量的操作不是原子操作。只有基本类型操作(赋值,读取)才是原子性操作。其他类型变量或基本类型的运算操作都是非原子操作。通过代码演示volatile修饰变量的非原子性操作:
public class SingletonInstance {    private volatile static SingletonInstance instance = null;    private SingletonInstance(){}    public static SingletonInstance getInstance (){        if(null == instance){                instance = new  SingletonInstance();        }        return instance;    }}

上述代码是想实现一个单例模式,即多线程环境下生成的SingletonInstance实例是唯一的。看看调用代码:

public class SingletonInstanceTest {    public static void main(String[] args) {        for(int i=0;i<30;i++){             new Thread(new ThreadDemo()).start();        }    }}class ThreadDemo implements Runnable{    public void run() {        System.out.println(SingletonInstance.getInstance());    }}

上述多线程环境下运行程序结果如下:
这里写图片描述
可以看到在上述环境下使用volatile修饰的变量并不能做到线程安全。就是因为里面涉及了非原子的操作。下面通过一张图来理解volatile的执行原理
这里写图片描述
1. 根据图中流程,线程A将主内存变量拷贝到工作内存经过运算之后再同步到主内存主要经过了8个步骤。
2. read:线程从主内存将共享变量var值(初始化一定在主内存完成)读取到工作内存,即结束。
3. load:线程执行完read操作之后就跟主内存变量没有关系。load是将主内存变量值赋给工作内存中的变量副本
4. use:使用load操作完工作内存中的变量副本,进行一定的运算。
5. assign:将use动作运算的结果赋值给变量副本var_v。
6. store:将新的运算结果的变量值存储到主内存。
7. write:作用于主内存变量,把store存储的值重新赋给主内存变量。
通过上述分析,可以发现volatile修饰的变量只是解决了变量在读取时对于所有 线程都是可见的。但是后续对于变量的操作不是原子性的。多线程对于volatile修饰 变量操作问题,还是需要通过加锁进行同步。这也就解释了上述程序中,为什么多线程环境中获得SingletonInstance的实例不是唯一的。
工作线程每次操作volatile修饰的变量,都需要先从主内存刷新最新的变量值,用于保证其他线程对该变量修改的可见性

可见性

  1. volatile修饰变量对于多线程读取时是可见的。也就是说如果线程A修改了共享变量var的值,线程B,C,D再次读取时,一定能够读取到最新的var值。因为使用volatile修饰的变量值,如果发生了改变,就会立即同步到内存。同理,其他线程每次读取该变量时都要重新从主内存读取。
    这里写图片描述

有序性

JVM中对于某一些规定有着天然有序性.这种有序性实在某些条件下才会成立的。在使用volatile修饰后,JVM遵从如下有序性:
1. 程序代码中volatile修饰变量V前的代码一定优先发生在V之前执行,不会在V之后执行。
2. 程序代码中V之后定义的代码一定在V之后执行,绝对不会在V之前执行。

总结

  1. volatile是JAVA中最轻量级的同步机制,只能是修饰变量,不能修饰方法和代码块
  2. volatile修饰的变量对于所有线程读取时是可见的,即一个线程对共享变量值的修改对于其他线程再次读取时是可见的。
  3. volatile修饰的变量在多线程环境中的操作是非原子性的,需要使用加锁进行同步。

参考资料

JAVA多线程编程核心艺术
深入理解JVM虚拟机
http://www.cnblogs.com/dolphin0520/p/3920373.html

原创粉丝点击