并发编程之volatile

1、Java内存模型内存交互操做

一、lock(锁定):做用于主内存的变量,把一个变量标记为一条线程独占状态java

二、unlock(解锁):做用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定缓存

三、read(读取):做用于主内存的变量,把一个变量值从主内存传输到线程的工做内存中,以便随后的load动做使用安全

四、load(载入):做用于工做内存的变量,它把read操做从主内存中获得的变量值放入工做内存的变量副本中多线程

五、use(使用):做用于工做内存的变量,把工做内存中的一个变量值传递给执行引擎并发

六、assign(赋值):做用于工做内存的变量,它把一个从执行引擎接收到的值赋给工做内存的变量高并发

七、store(存储):做用于工做内存的变量,把工做内存中的一个变量的值传送到主内存中,以便随后的write的操做性能

八、write(写入):做用于工做内存的变量,它把store操做从工做内存中的一个变量的值传送到主内存的变量中测试

整个执行流程如图优化

 

 read ---load  store----writr必须成对执行this

经过上面分析咱们能够看出即便在java里面执行i++这样的操做,对于咱们的底层来讲也不是原子操做,由于i++,也须要将这八大操做走一遍,具体来讲,read ---load 将主内存中i=0在工做内存中也copy一份,

线程读到工做内存中的i=0并加1操做即结果i=1写回工做内存(use---assign),而后将i=1写回主内存(store----writrt)这一步若是没有用缓存一致性协议,会有延时不会当即写到主内存,参考第一篇缓存一执行性协议讲解。

2、volatile原理与内存语义

volatile是Java虚拟机提供的轻量级的同步机制

volatile语义有以下两个做用

可见性:保证被volatile修饰的共享变量对全部线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值老是能够被其余线程当即得知。

有序性:禁止指令重排序优化。

volatile缓存可见性实现原理

JMM内存交互层面:volatile修饰的变量的read、load、use操做和assign、store、write必须是连续的,即修改后必须当即同步会主内存,使用时必须从主内存刷新,由此保证volatile变量的可见性。 底层实现:经过汇编lock前缀指令,它会锁定变量缓存行区域并写回主内存,这个操做称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存内存会致使其余处理器的缓存无效

3、volatile可见性分析

先上一段代码:

public class VolatileVisibilitySample {
    private boolean  initFlag = false;
    static Object object = new Object();

    public void refresh(){
        this.initFlag = true; //普通写操做,(volatile写)
        String threadname = Thread.currentThread().getName();
        System.out.println("线程:"+threadname+":修改共享变量initFlag");
    }

    public void load(){
        String threadname = Thread.currentThread().getName();
        int i = 0;
        while (!initFlag){
        }
        System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i);
    }

    public static void main(String[] args){
        VolatileVisibilitySample sample = new VolatileVisibilitySample();
        Thread threadA = new Thread(()->{
            sample.refresh();
        },"threadA");

        Thread threadB = new Thread(()->{
            sample.load();
        },"threadB");

        threadB.start();
        try {
             Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadA.start();
    }

}

代码很好理解,线程B读取成员变量initFlag 若是为false无线循环,若是为true,打出表示语,线程A负责将initFlag改成true,线程B先启动,线程A启动修改标志为true后,看看线程B可否感知到并终止循环

测试结果 :线程B无线循环,未能感知到标志被线程A修改,缘由,线程B一直读的是工做空间的缓存数据,当线程A修改数据以后,线程B未能感知到.

降上诉代码修改,线程B的执行任务上加锁synchronized:

 public void load(){
        String threadname = Thread.currentThread().getName();
        int i = 0;
        while (!initFlag){
            synchronized (object){
                i++;
            }
        }
        System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i);
    }

测试结果:加锁会致使线程B失去cpu执行权,当再次获取cpu执行权时,会引发线程上下文切换,这个过程会引发从新读取主内存数据。

volatile关键字测试

initFlag用volatile修饰后

 private volatile boolean  initFlag = false;

测试结果:当线程A修改initFlag后线程B能当即感知到,中止循环打出标志语;

缘由:线程A修改initFlag,因为initFlag被volatile修饰,会当即从工做内存刷到主内存,同时让其余线程中工做内存中initFlag数据缓存失效,这样线程B中原来地缓存失效,从主内存中从新读取新值。

4、volatile不能保证原子性

先来一段代码:

public class VolatileAtomicSample {

    private static volatile int counter = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    counter++; //不是一个原子操做,第一轮循环结果是没有刷入主存,这一轮循环已经无效
                    //1 load counter 到工做内存
                    //2 add counter 执行自加
                    //其余的代码段?
                }
            });
            thread.start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter);
    }

}

开10个线程每一个线程对counter进行1000次+最总咱们地结果也不是10000,而是小于10000,

缘由:couter++并非原子操做,好比两个线程读到counter=0都读到本身地工做内存,而后加1以后都要往咱们地主内存写,这时候必然引发裁决,致使一个线程的+1有效果,一个线程的+1无效果,最后致使

两个线程一共加了两次1,只有一个有效,最后结果比预期结果小。

5、volatile保证有序性防止指令重排

有序性问题

 在Java里面,能够经过volatile关键字来保证必定的“有序性”(具体原理在下一节讲述volatile关键字)。另外能够经过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每一个时刻是有一个线程执行同步代码,至关因而让线程顺序执行同步代码,天然就保证了有序性。

指令重排序:java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化状况的结果相等,那么指令的执行顺序能够与代码顺序不一致,此过程叫指令的重排序。指令重排序的意义是什么?JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能
 
as-if-serial语义的意思是:无论怎么重排序(编译器和处理器为了提升并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵照as-if-serial语义。为了遵照as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操做作重排序,由于这种重排序会改变执行结果。可是,若是操做之间不存在数据依赖关系,这些操做就可能被编译器和处理器重排序。也就是说指令重排只能保证单线程没有问题,不能保证多线程安全。
 

指令从排序发生在编译重排序和处理器重排序,禁止指令重排序的底层就是内存屏障,内存屏障分为4种

一、StoreStore  二、StoreLoad  三、LoadLoad  四、LoadStore

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来讲,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采起保守策略。下面是基于保守策略的JMM内存屏障插入策略。
(1)∙在每一个volatile写操做的前面插入一个StoreStore屏障。
(2)在每一个volatile写操做的后面插入一个StoreLoad屏障。
(3)在每一个volatile读操做的后面插入一个LoadLoad屏障。
(4)在每一个volatile读操做的后面插入一个LoadStore屏障。
上述内存屏障插入策略很是保守,但它能够保证在任意处理器平台,任意的程序中都能获得正确的volatile内存语义。
 
小知识点,不用volatile如何防止指令重排:
手动加内存屏障
 public void run() {
                    //因为线程one先启动,下面这句话让它等一等线程two. 读着可根据本身电脑的实际性能适当调整等待时间.
                    shortWait(10000);
                    a = 1; //是读仍是写?store,volatile写
                    //storeload ,读写屏障,不容许volatile写与第二部volatile读发生重排
                    //手动加内存屏障
                    UnsafeInstance.reflectGetUnsafe().storeFence();
                    x = b; // 读仍是写?读写都有,先读volatile,写普通变量
                    //分两步进行,第一步先volatile读,第二步再普通写
                }
            });

6、总线风暴问题

大量使用volatile会引发工做缓存有大量的无效缓存,并且volatile会一块儿会引发线程之间相互监听,嗅探,这些都会占用总线资源,致使总线资源负载太高。这时候咱们须要锁来解决问题,这就是为何有了

volatile咱们还须要synchronized,lock锁,由于volatile保证不了原子操做,且用的过多会致使总线风暴。

7、volatile,synchronized同时使用-----一个超高并发的单例场景

public class Singleton {

    /**
     * 查看汇编指令
     * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
     */
    private volatile static Singleton myinstance;

    public static Singleton getInstance() {
        if (myinstance == null) {
            synchronized (Singleton.class) {
                if (myinstance == null) {
                    myinstance = new Singleton();//对象建立过程,本质能够分文三步
                    //对象延迟初始化
                    //
                }
            }
        }
        return myinstance;
    }

    public static void main(String[] args) {
        Singleton.getInstance();
    }
}

解释:建立对象myinstance = new Singleton() 并不时一个原子操做,它能够分为三部,一、申请空间,2,实力化对象,3,地址赋值给myinstance 变量,加synchronized 保证了原子操做,可是没法防止指令重排,线程1申请完空间以后若是发生指令重排直接执行第3步赋值,那么线程2执行if判断时myinstance 不为空可是却没有实例化对象。这是指令重排致使的,因此volatile 修饰myinstance防止发生指令重排。----超高并发下的应用。

相关文章
相关标签/搜索