zoukankan      html  css  js  c++  java
  • 面试系列-volatile关键字详解

    作者:海子https://www.cnblogs.com/dolphin0520/p/3920373.html

    Java架构师之路做了编排和配图

    volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用volatile关键字的场景。

    内存模型

    程序在执行过程中,每条指令都是通过CPU来执行的,存在数据的读取和写入。程序运行过程中的临时数据存放在主内存(物理内存)中,cpu执行速度很快,要是直接从主内存中读取和写入数据,那执行指令的速度要慢很多,所以cpu要引入高速缓存也叫工作内存。

    程序实际运行过程,会将运算需要的数据从主内存中复制一份到cpu高速缓存当中,直接从高速缓存中读取和写入数据,运算结束后再把高速缓存的数据刷新到主内存当中。

    多线程共享变量

    每个线程都有自己独立的工作内存,多个线程同时访问一个变量会存在缓存一致性问题。假如X的初始值为1,可能存在线程1和2都执行X=X+1,cpu执行时工作内存1和工作内存2中X的值都为1,两个线程执行完后刷新到主内存,最终导致执行结果为2,而不是3,。如下图:

    640?wx_fmt=png

    如果线程1对变量的修改能够被线程二看到,需要做如下操作:

    1.线程1把修改后的变量从工作内存1中刷新到主内存中

    2.主内存把最新的变量值更新到线程2的工作内存中

    并发编程

    并发编程中有三个问题:原子性可见性有序性

    原子性

    原子性:即一个操作或者多个操作要么全部执行且执行过程中不会被打断,要么就都不执行。举例从账户A向账户B转1000元,需要先从账户A减去1000元,再往账户B加上1000元。如果没有原子性保护,从账户A减去1000元,操作突然中止。导致A账户少了1000元,而B账户没有加上1000元。

    可见性

    可见性:指多个线程访问同一变量时,一个线程修改了变量值,其他线程能够立即看到修改的值。

    举例

    //线程1执行的代码
    int i = 0;
    i = 10;
    //线程2执行的代码
    int j = i;

    线程1执行完后i的值为10,线程2执行完后j的值仍为0,这就是可见性问题。

    有序性

    有序性指程序执行的顺序按照代码的先后顺序执行。

    举例

    int i = 0;
    int j = 0;
    i = 1;   //语句1
    j = 2;   //语句2

    程序在执行时不一定语句1先执行,语句2后执行,因为cpu对执行进行了重排序,因为处理器为了提高程序的运行效率,可能会对输入的代码进行优化,就不能保证执行的先后顺序和代码的先后顺序一样,但能保证执行的结果和代码顺序执行结果一致。

    剖析volatile关键字

    以上的讲述都是深入理解volatile关键字做铺垫,下边进入正题。

    volatile关键字两层语义

    一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,就具备了两层语义:

    1.保证了不同线程对变量进行操作时的可见性,即一个线程修改了变量的值,修改后的值对其他线程来说是立即可见的。

    2.禁止进行指令重排序。

    volatile不能保证原子性

    如下代码示例:

        public volatile int num = 0;
        public void increase(){
            num++;
        } 
        public static void main(String[] args) throws InterruptedException {
            VolatileTest volatileTest = new VolatileTest();
            for (int i=0; i<10; i++){
                new Thread(){
                    @Override
                    public void run(){
                        for (int j=0; j<1000; j++){
                            volatileTest.increase();
                        }
                    }
                }.start();
            }
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(volatileTest.num);
        }
    }
    

    运行结果发现每次都不一样,我们期望的值是:10000,导致原因如下:

    自增操作不是原子性的,而且volatile也无法保证对变量操作的原子性

    如想保证原子性操作可以使用如下方法:

    1.increase方法前加synchronized关键字。

    2.increase方法中加锁。

    3.使用AtomicInteger类。

    volatile原理及实现机制

    下面这段话摘自《深入理解Java虚拟机》:

      “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

    lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

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

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

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

    volatile使用场景

    synchronized关键字是防止多个线程同时执行一段代码,但会很影响执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

    1.对变量的写操作不依赖于当前值.

    2.该变量没有包含在具有其他变量的不变式中.

    我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字在并发时能够正确执行。

    往期推荐

     

    【技术篇】 

    【技术篇】 

    【生活篇】 

    640?wx_fmt=jpeg

  • 相关阅读:
    iOS开发UI篇—控制器的View的创建
    iOS开发UI篇—控制器的创建
    iOS开发UI篇—UIWindow简单介绍
    iOS开发UI篇—导航控制器属性和基本使用
    按照官网的升级完socket.io报错Manager is being released。
    npm和yarn的区别,我们该如何选择?
    iOS性能优化-数组、字典便利时间复杂
    OC CollectionView和TableView自身高度的隐式递归计算,改变父试图布局
    阿里系手淘weex学习第一天
    iOS邓白氏编码申请流程及苹果账号组织名称变更
  • 原文地址:https://www.cnblogs.com/Java-Road/p/11824682.html
Copyright © 2011-2022 走看看