volatile

首先看看如下代码以及结果:(计算一秒钟内count++的结果)

public class VolatileDemo {
    private static Boolean flag = true;   //创建一个状态变量flag
    public static void main(String[] args) {
        Thread thread = new Thread(()-> {   //创建一个线程
            int count = 0;
           while (flag){
               count++;
           }
           System.out.println("count:"+count);
        });
        Thread thread1 = new Thread(()->{//创建一个线程
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = false;
            System.out.println("1秒结束");
        });

        thread1.start();
        thread.start();
    }
}

 由运行结果可见,程序一直未停下来,说明变量flag修改无法实时同步给另一个线程。

当加入volatile关键字修饰状态变量flag:

public class VolatileDemo {
    private volatile static Boolean flag = true;
    public static void main(String[] args) {
        Thread thread = new Thread(()-> {
            int count = 0;
           while (flag){
               count++;
           }
           System.out.println("count:"+count);
        });
        Thread thread1 = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = false;
            System.out.println("1秒结束");
        });

        thread1.start();
        thread.start();
    }
}

 

 为什么会这样?

       在不加volatile关键时,线程堆栈中保存线程运行时的变量值的副本,当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样。

volatile的特征

  • 禁止重排序    代码在执行过程中,为了提高代码的执行效率,会对代码做优化,编译、字节码,机器码、汇编都会对代码进行优化,代码优化的原则是优化前后是不会来影响执行结果;Java内存模型不会对volatile指令进行重排序,从而保证对volatile变量的执行顺序,永远是按照其出现顺序执行的。重排序的依据是happens-before法则。
  • 保证内存的可见性   

 工作原理:

        在volatile关键字所修饰的变量时,在汇编层代码上,会添加一个lock前缀的指令,Lock前缀指令相当于添加了一个内存屏障,内存屏障提供的功能:

1、它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

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

3、如果是写操作,它会导致其他CPU中对应的缓存行无效。

禁止指令重排序的原理

       通过内存屏障实现,StoreStoreStoreLoadLoadLoadLoadStore屏障,保证写与写、写与读、读与读、读与写等有序。

可见性原理

       解决内存一致性问题:JVM向处理器发送一个Lock前缀的指令,两种解决方案:

第一种:通过总线加LocK锁前缀的方法锁定总线。

第二种:通过缓存一致性协议(MESI协议)(缓存行 维护两个状态位 M,E,S,I)。

M(被修改的):处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。

E(独占的):处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改, 即与内存中一致。

S(共享的):处于这一状态的数据在多个CPU中都有缓存,且与内存一致。

I(无效的):本CPU中的这份缓存已经无效。

缓存一致性协议执行过程:

      一个处于E (独占的)状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S;一个处于S (共享的)状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。一个处于M (被修改的)状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。

        当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取(无效的就要读取新值),并把自己状态变成S (既然是无效那么说明已有cpu对缓存做了修改),如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果,如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。

普通变量:

1,变量值从主内存(在堆中)load到本地内存(在当前线程的栈桢中);

2,之后,线程就不再和该变量在主内存中的值由任何关系,而是直接操作在副本变量上(这样速度很快),这时,如果主存中的count或本地内存中的副本发生任何变化,都不会影响到对方,也正是这个原因导致并发情况下出现数据不一致;

3,在修改完的某个时刻(线程退出之前),自动把本地内存中的变量副本值回写到对象在堆中的对应变量。

volatile修饰的变量

volatile仍然在执行一个从主存加载到工作内存,并且将变更的值写回主存的操作,但是:

1,volatile保证每次读取该变量前,都判断当前值是否已经失效(即是否已经与主存不一致),如果已经失效,则从主存load最新的变量;

2,volatile保证每次对该变量做出修改时,都立即写入主存。

注意:volatile保证共享数据的可见性,有序性,却无法保证数据的原子性。

应用场景

  • 一般用来修饰Boolean类型的共享状态标志位。
  • 单例模式下的双重校验锁。
  • 修饰单个的变量。

注意:volatile对于基本数据类型(值直接从主内存向工作内存copy)才有用。但是对于对象来说,似乎没有用,因为volatile只是保证对象引用的可见性,而对对象内部的字段,它保证不了任何事。即便是在使用ThreadLocal时,每个线程都有一份变量副本,这些副本本身也是存储在堆中的,线程栈桢中保存的仍然是基本数据类型和变量副本的引用。一个对象被volatile修饰,那么就表示它的引用具有了可见性。从而使得对于变量引用的任何变更,都在线程间可见,但是却不具备原子性。

原文地址:https://www.cnblogs.com/128-cdy/p/12501454.html