# 深刻理解volatile

深刻理解volatile

Volatile的官方定义

Java语言规范第三版中对volatile的定义以下:java编程语言容许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保经过排他锁单独得到这个变量。Java语言提供了volatile,在某些状况下比锁更加方便。若是一个字段被声明成volatile,java线程内存模型确保全部线程看到这个变量的值是一致的。
volatile变量修饰符若是使用恰当的话,它比synchronized的使用和执行成本会更低,由于它不会引发线程上下文的切换和调度。java

可见性

处理器为了提升处理速度,不直接和内存进行通信,而是先将系统内存的数据读到内部缓存L1,L2或其余)后再进行操做,但操做完以后不知道什么时候会写到内存,若是对声明了Volatile变量进行写操做,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。可是就算写回到内存,若是其余处理器缓存的值仍是旧的,再执行计算操做就会有问题,因此在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每一个处理器经过嗅探在总线上传播的数据来检查本身缓存的值是否是过时了,当处理器发现本身缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操做的时候,会强制从新从系统内存里把数据读处处理器缓存里。c++

缓存一致性解决方案

数据总线加锁

LOCK前缀指令会引发缓存回写到内存,LOCK前缀指令致使执行指令期间,声明处理器的LOCK#信号。在多处理器环境中,LOCK# 信号确保在声言该信号期间,处理器能够独占使用任何共享内存。(由于它会锁住总线,致使其余CPU不能访问总线,不能访问总线就意味着不能访问系统内存)程序员

缓存一致性协议

LOCK#信号通常不锁总线,而是锁缓存,毕竟锁总线开销比较大。对于Intel486和Pentium处理器,在锁操做时,老是在总线上声言LOCK#信号。但在P6和最近的处理器中,若是访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反地,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操做被称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据 。
一个处理器的缓存回写到内存会致使其余处理器的缓存无效 。IA-32处理器和Intel 64处理器使用MESI(修改,独占,共享,无效)控制协议去维护内部缓存和其余处理器缓存的一致性。在多核处理器系统中进行操做的时候,IA-32 和Intel 64处理器能嗅探其余处理器访问系统内存和它们的内部缓存。它们使用嗅探技术保证它的内部缓存,系统内存和其余处理器的缓存的数据在总线上保持一致。例如在Pentium和P6 family处理器中,若是经过嗅探一个处理器来检测其余处理器打算写内存地址,而这个地址当前处理共享状态,那么正在嗅探的处理器将无效它的缓存行,在下次访问相同内存地址时,强制执行缓存行填充。编程

内存模型

在这里插入图片描述

代码示列

public class VolatileTest {
    //可见性
    private /**volatile**/ static int INIT_VALUE = 0;

    private final static int MAX_VALUE = 5;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            int localVale = INIT_VALUE;
            while (localVale < MAX_VALUE) {
                /***  对INIT_VALUE没有volatile关键字修  ***/
                //为何这里一直没有去从主内存中拿数据进行刷新呢?
                //这是由于java认为这里没有writer的操做,因此不须要去主内存中获取新的数据。这个具体最新的值被刷新指
                //具体能够VolatileTest2进行比较
                //在这里加一个sysytem的输出是有可能会去刷新主存的,或者每次运行的时候休眠一小段时间,
                // 这个程序是有可能会结束的。若是没有System的输出,或者休眠,在while判断会一直不去主内存
                //刷新新数据,也就致使程序一直无法结束。
                //System.out.println("=");
                /*try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
                if (localVale != INIT_VALUE) {
                    System.out.println("The value updated to [ " + INIT_VALUE + " ]");
                    localVale = INIT_VALUE;
                }
            }
        },"READER").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            int localValue = INIT_VALUE;
            while (INIT_VALUE < MAX_VALUE) {
                System.out.println("update the value to [ " + (++localValue) + " ]");
                INIT_VALUE = localValue;
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"UPDATER").start();
    }
}

有序性

happens-before规则

Java的内存模型具有一些天生的有序规则,不须要任何同步手段就可以保证的有序性,这个规则被称为Happens-before原则,若是两个操做的执行顺序没法从happens-before原则推导出来,那么它们就没法保证有序性,也就是说虚拟机或处理器能够随意对它们进行重排序处理。缓存

  1. 程序次序规则:在一个线程内,代码按照编写时的次序执行,编写在后面的操做发生于编写在前面的操做以后(若是这个都不能保证,咱们程序员还怎么编程呢????对吧,因此这个确定是须要保证的)
    这个规则的意思就是程序按照编写的顺序来执行,可是虚拟机仍是可能会对程序代码的指令进行重排序,只要确保在一个线程内最终的结果和代码顺序执行的结果一致便可。
  2. 锁定规则:一个unlock操做要先行发生于同一个锁的lock操做
    这句话的意思是,不管是单线程仍是多线程的环境下,若是同一个锁是锁定状态,那必须先对其执行释放操做以后才能继续执行lock操做。
  3. Volatile变量规则:对一个变量的写操做要早于对这个变量以后的读操做
    这句话的意思是,若是一个变量使用volatile关键字修饰,一个线程对它进行读操做,一个线程对他进行写操做,那么写入操做确定要先行于读操做。
  4. 传递规则:若是操做A先于操做B,而操做B又先于操做C,则能够得出操做A确定先于操做C。
  5. 线程启动规则:Thread对象的start()方法先行发生于对该线程的任何动做,只有start以后线程才能真正运行,不然Thread也只是一个对象而已。
  6. 线程中断规则:对线程执行interrupt()方法确定要优先于捕获到中断信号。若是线程收到了中断信号,那么在此以前势必要有interrupt()。
  7. 线程终结规则:线程中全部的操做都要先行发生于线程的终止检测,通俗的讲,线程的任务执行、逻辑单元执行确定要发生于线程死亡以前。
  8. 对象终结规则:一个对象的初始化完成先行于finalize()方法以前。

指令重排序

由程序次序规则,在单线程的状况下,对于指令重排序不会出现什么问题,可是对于多线程的状况下,就颇有可能会因为指令重排序出现问题。
volatile关键字直接禁止JVM和处理器对volatile关键字修饰的指令重排序,可是对于volatile先后五以来的指令则能够随便怎么排序多线程

int x = 10
int y = 20
/**
  在语句volatile int z = 20以前,先执行x的定义仍是先执行y的定义,咱们并不关心,只要可以百分百
  保证在执行到z=20的时候,x=0,y=1已经定义好,同理对于x的自增以及y的自增操做都必须在z=20之后才能发生,这个规则能够认为是由程序次序规则+volatile规则推导
**/
valatile int z = 20
x++;
y++;
private volatile boole init = false;
private Context context ;
public Context context() {
    if(!init){
        context = loadContext();
        /**
            若是init不使用volatile关键字修饰的话,因为编译器会对指令作必定的优化,也就是指令重排序。
            因此在由多线程执行的状况下,如某个线程A它可能执行init = true,后执行context = loadContext(),
            由于这两条指令并无任何的依赖关系,因此执行顺序可能不定。当线程B执行到判断的时候,发现init=true成立,
            那么线程B就不会再去加载context啦,此时若是它使用context,有可能context在线程A中尚未加载成功,此时线程B去
            使用context就有可能报空指针异常。
            而volatile关键字能阻止指令重排序,也就是说在init=true以前必定保证context=loadContext()执行完毕。
        **/
        init = true; //阻止指令重排序
    }
}

其实被volatile修饰的变量存在一个“lock”的前缀。
lock前缀实际上至关因而一个内存屏障,该内存屏障会为指令的执行提供以下几个保障
1.确保指令重排序不会将其后面的代码排到内存屏障以前
2.确保指令重排序不会将其前面的代码拍到内存屏障以后。
3.确保在执行内存屏障修饰的指令时前面的代码所有执行完成(1,2,3阻止了指令重排序)
4.强制将线程工做内存中的修改刷新至主内存中
5.若是是写操做,则会致使其余线程的工做内存(CPU Cache)中的缓存数据失效。(4,5保证了内存可见性)并发

原子性

volatile没法保证原子性
原子性:一个操做或多个操做,要么都成功,要么都失败,中间不能因为任何的因素中断
对基本数据类型的变量读取和赋值是保证了原子性的,要么都成功,要么都失败,这些操做不可被中断
a = 10; 原子性
b = a; 不知足1.read a; 2.assign to b;
c++; 不知足1.read c; 2.add 3.assign to c
c = c + 1; 不知足1.read c; 2.add 3.assign to capp

public class VolatileTest2 {

    //虽然保证了可见性,可是没有保证原子性
    private volatile static int INIT_VALUE = 0;

    private final static int MAX_VALUE = 50;

    public static void main(String[] args) {
        new Thread(() -> {
            while (INIT_VALUE < MAX_VALUE) {
                //颇有可能会输出重复的数字
                System.out.println("ADD-1-> " + (++INIT_VALUE));
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ADD-1").start();

        new Thread(() -> {
            while (INIT_VALUE < MAX_VALUE) {
                System.out.println("ADD-2-> " + (++INIT_VALUE));
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ADD-2").start();

    }
}

volatile与synchronized

(1) 使用上的区别jvm

  • volatile只能用于修饰实例变量或类变量,不能用于修饰方法以及方法参数和局部变量,常量等。
  • synchronized关键字不能用于对变量的修饰,只能用于修饰方法或者语句块。
  • volatile修饰的变量能够为null,synchronized关键字同步语句块的moniter对象不能为null。
    (2)对原子性的保证
  • volatile没法保证原子性
  • 因为synchronized是一种排他机制,所以synchronized关键字修饰的同步代码是没法被中断,所以可以保证其原子性。
    (3)可见性的保证
    二者都可以保证共享资源在多线程间的可见性,可是实现机制彻底不一样
    synchronized借助于jvm指令的monitor enter和moniter exit对经过排他的方式使得同步代码串行化,在monitor exit时全部共享资源都会被刷新到主存中;
    volatile使用机器指令(lock;)的方式迫使其余线程工做内存的数据失效,不得不到主存中从新加载数据
    (4)对有序性的保证
    二者都保证有序性,volatile关键字禁止jvm编译器以及处理器对其进行重排序,因此他可以保证有序性
    synchronized以程序的串行化执行来保证有序性

参考博客

并发之volatile底层原理:http://www.javashuo.com/article/p-fgsreuii-a.html

相关文章
相关标签/搜索