zoukankan      html  css  js  c++  java
  • Java多线程之内存可见性和原子性操作 二 volatile

    Java多线程之内存可见性和原子性操作 一 synchronize

     Volatile实现可见性

    通过内存屏障和禁止指令重排序实现可见性

    写操作的时候,会把内容强制刷新到主内存中去

    读操作的时候,会强制吧缓冲区的内容清掉,然后从主内存读取最新值。

    在java中一共有八条操作指令,store和load是其中的两条。

    volatile的通俗理解

    volatile变量在每次被线程访问的时候,都强迫从主内存中重读该变量的值,而当该变量发生变化的时候,又会强迫线程将更新的值刷新到主内存。

    这样任何时刻,不同的线程总能看到该变量的最新值。

    线程写volatile变量的过程

    1.改变线程工作内存中volatile变量副本的值

    2.将改变的副本的值从工作内存刷新到主内存

    线程读volatile变量的过程

    1.从主内存中读取volatile变量的最新值到线程的工作内存中

    2.从工作内存中读取volatile的变量的副本

    volatile不能保持原子性

    变量自增操作解释volatile不能保证原子性

    number++可以分解成三个步骤【重】,所以是线程不安全的,所以用sync加上锁后就是原子操作,三个步骤就合成了一个步骤,必须同时执行。

    但是如果用volatile修饰的话:

    举个例子:volatile修饰number

    有两个线程A和B,

    首先a抢到cpu,读取num的值;读完(读操作不会将工作内存的值立刻刷新到主内存,只有对num写才会刷新到主内存中)之后让出cpu,b抢到cpu,a线程阻塞,然后b读取,b执行num+1操作,这时候由于volatile的对内存可见性,会把最新值刷新到主内存中去,所以这时候主内存中num的值为6。

    可是刚刚a读取num的值的时候num还是5.这时候如果b让出cpu,a抢到cpu的 时候不会重新读num的值,而是直接执行+1操作了。

     

    从第五步开始A执行+1操作,num本来主内存已经是6了,现在又从5+1变成6。可见两个线程分别对num+1,可是看起来,只加了一次。

    这是由于volatile不能保持原子性

    实现原子性的解决方法

    第一种方法:

     锁整个方法:

    这个方式,使得效率比较低,因为休眠100毫秒,B线程必须等待完A休眠完才行

    锁代码块:

    这样,number的自增操作就保证了原子性,且效率更高

    第二种方法(实现可见性 原子性)

    1.

     2.

     

    3.

     volatile禁止指令重排序

    https://blog.csdn.net/javazejian/article/details/72772461

    理解java内存区域与java内存模型

    volatile禁止重排优化

    既然volatile不能保证原子性,那他有什么优点呢?

    volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如何实现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)。 
    内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。下面看一个非常典型的禁止重排优化的例子DCL,如下:

    /**
     * Created by zejian on 2017/6/11.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     */
    public class DoubleCheckLock {
    
        private static DoubleCheckLock instance;
    
        private DoubleCheckLock(){}
    
        public static DoubleCheckLock getInstance(){
    
            //第一次检测
            if (instance==null){
                //同步
                synchronized (DoubleCheckLock.class){
                    if (instance == null){
                        //多线程环境下可能会出现问题的地方
                        instance = new DoubleCheckLock();
                    }
                }
            }
            return instance;
        }
    }

    上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)

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

    由于步骤1和步骤2间可能会重排序,如下:

    memory = allocate(); //1.分配对象内存空间
    instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
    instance(memory);    //2.初始化对象

    由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。那么该如何解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。

      //禁止指令重排优化
      private volatile static DoubleCheckLock instance;

    ok~,到此相信我们对Java内存模型和volatile应该都有了比较全面的认识,总而言之,我们应该清楚知道,JMM就是一组规则,这组规则意在解决在并发编程可能出现的线程安全问题,并提供了内置解决方案(happen-before原则)及其外部可使用的同步手段(synchronized/volatile等),确保了程序执行在多线程环境中的应有的原子性,可视性及其有序性。

     volatile适用场景

    第二点意思是如果有两个volatile变量,每个volatile变量状态要独立于其他volatile变量,具体啥意思也没实际用过,总之,感觉volatile没有sync实用

     synchronized和volatile的比较

    所以在保证volatile适用场景的情况下,实用volatile更加轻量级。

    注:final也可保证可见性,因为他修饰的变量本身就是不能被改变的。

    补充一点

    如果想要避免这种情况,就用关键字volatile声明64位的变量,或者把对他们的读写操作锁起来。

  • 相关阅读:
    Response生成注册验证码实现例子02
    Mysql 自增字段起始值auto_increment的修改方法
    elite核心库的加载方式及自动加载类库
    elite核心类库之事件类
    wamp速度缓慢的解决办法
    Dwzdialog中批量提交的问题处理
    PHP中缀表达式与逆波兰式的计算(用于工资项目等四则计算)
    PHP工资计算之逆波兰式
    elite核心类库之模板类
    PHP soap访问接口出错汇总及解决办法
  • 原文地址:https://www.cnblogs.com/volvane/p/13531121.html
Copyright © 2011-2022 走看看