返回顶部
首页 > 资讯 > 后端开发 > Python >Java基础面试题之volatile详解
  • 806
分享到

Java基础面试题之volatile详解

2024-04-02 19:04:59 806人浏览 八月长安

Python 官方文档:入门教程 => 点击学习

摘要

目录1、volatile保证可见性1.1、什么是JMM模型?1.2、volatile保证可见性的代码验证1.2.1、无可见性代码验证1.2.1、volatile保证可见性验证2、vo

问:请谈谈你对volatile的理解?

答:volatile是Java虚拟机提供的轻量级的同步机制,它有3个特性:

1)保证可见性

2)不保证原子性

3)禁止指令重排

刚学完Java基础,如果有人问你什么是volatile?它有什么作用的话,相信一定非常懵逼…

可能看了答案,也完全不明白,什么是同步机制?什么是可见性?什么是原子性?什么是指令重排?

1、volatile保证可见性

1.1、什么是JMM模型?

要想理解什么是可见性,首先要先理解JMM。

JMM(Java内存模型,Java Memory Model)本身是一种抽象的概念,并不真实存在。它描述的是一组规则或规范,通过这组规范,定了程序中各个变量的访问方法。JMM关于同步的规定:

1)线程解前,必须把共享变量的值刷新回主内存;

2)线程加锁前,必须读取主内存的最新值到自己的工作内存;

3)加锁解锁是同一把锁;

由于JVM运行程序的实体是线程,创建每个线程时,JMM会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域。

Java内存模型规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。

但线程对变量的操作(读取、赋值等)必须在工作内存中进行。因此首先要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写会主内存中。

看了上面对JMM的介绍,可能还是优点懵,接下来用一个卖票系统来进行举例:

1)如下图,此时卖票系统后端只剩下1张票,并已读入主内存中:ticketNum=1。

2)此时网络上有多个用户都在抢票,那么此时就有多个线程同时都在进行买票服务,假设此时有3个线程都读入了目前的票数:ticketNum=1,那么接着就会买票。

3)假设线程1先抢占到cpu的资源,先买好票,并在自己的工作内存中将ticketNum的值改为0:ticketNum=0,然后再写回到主内存中。

此时,线程1的用户已经买到票了,那么线程2,线程3此时应该不能再继续买票了,因此需要系统通知线程2,线程3,ticketNum此时已经等于0了:ticketNum=0。如果有这样的通知操作,你就可以理解为就具有可见性。

通过上面对JMM的介绍和举例,可以简单总结下。

JMM内存模型的可见性是指,多线程访问主内存的某一个资源时,如果某一个线程在自己的工作内存中修改了该资源,并写回主内存,那么JMM内存模型应该要通知其他线程来从新获取最新的资源,来保证最新资源的可见性。

1.2、volatile保证可见性的代码验证

在1.1中,已经基本理解了可见性的含义,接下来用代码来验证一下,volatile确实可以保证可见性。

1.2.1、无可见性代码验证

首先先验证下,不使用volatile,是不是就是没有可见性。

package com.koping.test;

import java.util.concurrent.TimeUnit;

class MyData
{
    int number = 0;

    public void add10() {
        this.number += 10;
    }
}

public class VolatileVisibilityDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 启动一个线程修改myData的number,将number的值加10
        new Thread(
                () -> {
                    System.out.println("线程" + Thread.currentThread().getName()+"\t 正在执行");
                    try{
                        TimeUnit.SECONDS.sleep(3);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    myData.add10();
                    System.out.println("线程" + Thread.currentThread().getName()+"\t 更新后,number的值为" + myData.number);
                }
        ).start();

        // 看一下主线程能否保持可见性
        while (myData.number == 0) {
            // 当上面的线程将number加10后,如果有可见性的话,那么就会跳出循环;
            // 如果没有可见性的话,就会一直在循环里执行
        }
        System.out.println("具有可见性!");
    }
}

运行结果如下图,可以看到虽然线程0已经将number的值改为了10,但是主线程还是在循环中,因为此时number不具有可见性,系统不会主动通知。

1.2.1、volatile保证可见性验证

在上面代码的第7行给变量number添加volatile后再次测试,如下图,此时主线程成功退出了循环,因为JMM主动通知了主线程更新number的值了,number已经不为0了。

2、volatile不保证原子性

2.1 什么是原子性?

理解了上面说的可见性之后,再来理解下什么叫原子性?

原子性是指不可分隔,完整性,即某个线程正在做某个业务时,中间不能被分割。要么同时成功,要么同时失败。

还是有点抽象,接下来举个例子。

如下图,创建了一个测试原子性的类:TestPragma。在add方法中将n加1,通过查看编译后的代码可以看到,n++被拆分为3个指令进行执行。

因此可能存在线程1正在执行第1个指令,紧接着线程2也正在执行第1个指令,这样当线程1和线程2都执行完3个指令之后,很容易理解,此时n的值只加了1,而实际是有2个线程加了2次,因此这种情况就是不保证原子性。

2.2 不保证原子性的代码验证

在2.1中已经进行了举例,可能存在2个线程执行n++的操作,但是最终n的值却只加了1的情况,接下来对这种情况再用代码进行演示下。

首先给MyData类添加一个add方法

package com.koping.test;

class MyData {
    volatile int number = 0;

    public void add() {
        number++;
    }
}

然后创建测试原子性的类:TestPragmaDemo。测试下20个线程给number各加1000次之后,number的值是否是20000。

package com.koping.test;

public class TestPragmaDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000
        for (int i=0; i<20; i++) {
            new Thread(() -> {
                for (int j=0; j<1000; j++) {
                    myData.add();
                }
            }).start();
        }

        // 程序运行时,模型会有主线程和守护线程。如果超过2个,那就说明上面的20个线程还有没执行完的,就需要等待
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);
    }
}

运行结果如下图,最终number的值仅为18410。

可以看到即使加了volatile,依然不保证有原子性。

2.3 volatile不保证原子性的解决方法

上面介绍并证明了volatile不保证原子性,那如果希望保证原子性,怎么办呢?以下提供了2种方法

2.3.1 方法1:使用synchronized

方法1是在add方法上添加synchronized,这样每次只有1个线程能执行add方法。

结果如下图,最终确实可以使number的值为20000,保证了原子性。

但是,实际业务逻辑方法中不可能只有只有number++这1行代码,上面可能还有n行代码逻辑。现在为了保证number的值是20000,就把整个方法都加锁了(其实另外那n行代码,完全可以由多线程同时执行的)。所以就优点杀鸡用牛刀,高射炮打蚊子,小题大做了。

package com.koping.test;

class MyData {
    volatile int number = 0;

    public synchronized void add() {
      // 在n++上面可能还有n行代码进行逻辑处理
        number++;
    }
}

2.3.2 方法1:使用JUC包下的AtomicInteger

给MyData新曾一个原子整型类型的变量num,初始值为0。

package com.koping.test;

import java.util.concurrent.atomic.AtomicInteger;

class MyData {
    volatile int number = 0;

    volatile AtomicInteger num = new AtomicInteger();

    public void add() {
        // 在n++上面可能还有n行代码进行逻辑处理
        number++;
        num.getAndIncrement();
    }
}

让num也同步加20000次。结果如下图,可以看到,使用原子整型的num可以保证原子性,也就是number++的时候不会被抢断。

package com.koping.test;

public class TestPragmaDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000
        for (int i=0; i<20; i++) {
            new Thread(() -> {
                for (int j=0; j<1000; j++) {
                    myData.add();
                }
            }).start();
        }

        // 程序运行时,模型会有主线程和守护线程。如果超过2个,那就说明上面的20个线程还有没执行完的,就需要等待
        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);
        System.out.println("num值加了20000次,此时number的实际值是:" + myData.num);
    }
}

3、volatile禁止指令重排

3.1 什么是指令重排?

在第2节中理解了什么是原子性,现在要理解下什么是指令重排?

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排:
源代码–>编译器优化重排–>指令并行重排–>内存系统重排–>最终执行指令

处理器在进行重排时,必须要考虑指令之间的数据依赖性。

单线程环境中,可以确保最终执行结果和代码顺序执行的结果一致。

但是多线程环境中,线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测

看了上面的文字性表达,然后看一个很简单的例子。

比如下面的mySort方法,在系统指令重排后,可能存在以下3种语句的执行情况:

1)1234

2)2134

3)1324

以上这3种重排结果,对最后程序的结果都不会有影响,也考虑了指令之间的数据依赖性。

public void mySort() {
    int x = 1;  // 语句1
    int y = 2;  // 语句2
    x = x + 3;  // 语句3
    y = x * x;  // 语句4
}

3.2 单线程单例模式

看完指令重排的简单介绍后,然后来看下单例模式的代码。

package com.koping.test;
public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }

    public static void main(String[] args) {
        // 单线程测试
        System.out.println("单线程的情况测试开始");
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println("单线程的情况测试结束\n");
    }
}

首先是在单线程情况下进行测试,结果如下图。可以看到,构造方法只执行了一次,是没有问题的。

3.3 多线程单例模式

接下来在多线程情况下进行测试,代码如下。

package com.koping.test;

public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }

        // DCL(Double Check Lock双端检索机制)
//        if (instance == null) {
//            synchronized (SingletonDemo.class) {
//                if (instance == null) {
//                    instance = new SingletonDemo();
//                }
//            }
//        }
        return instance;
    }

    public static void main(String[] args) {
        // 单线程测试
//        System.out.println("单线程的情况测试开始");
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//        System.out.println("单线程的情况测试结束\n");

        // 多线程测试
        System.out.println("多线程的情况测试开始");
        for (int i=1; i<=10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}

在多线程情况下的运行结果如下图。可以看到,多线程情况下,出现了构造方法执行了2次的情况。

3.4 多线程单例模式改进:DCL

在3.3中的多线程单里模式下,构造方法执行了两次,因此需要进行改进,这里使用双端检锁机制:Double Check Lock, DCL。即加锁之前和之后都进行检查。

package com.koping.test;

public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {
//        if (instance == null) {
//            instance = new SingletonDemo();
//        }

        // DCL(Double Check Lock双端检锁机制)
        if (instance == null) {  // a行
            synchronized (SingletonDemo.class) {
                if (instance == null) {  // b行
                    instance = new SingletonDemo();  // c行
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        // 单线程测试
//        System.out.println("单线程的情况测试开始");
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//        System.out.println("单线程的情况测试结束\n");

        // 多线程测试
        System.out.println("多线程的情况测试开始");
        for (int i=1; i<=10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}

在多次运行后,可以看到,在多线程情况下,此时构造方法也只执行1次了。

3.5 多线程单例模式改进,DCL版存在的问题

需要注意的是3.4中的DCL版的单例模式依然不是100%准确的!!!

是不是不太明白为什么3.4DCL版单例模式不是100%准确的原因

是不是不太明白在3.1讲完指令重排的简单理解后,为什么突然要讲多线程的单例模式

因为3.4DCL版单例模式可能会由于指令重排而导致问题,虽然该问题出现的可能性可能是千万分之一,但是该代码依然不是100%准确的。如果要保证100%准确,那么需要添加volatile关键字,添加volatile可以禁止指令重排

接下来分析下,为什么3.4DCL版单例模式不是100%准确?

查看instance = new SingletonDemo();编译后的指令,可以分为以下3步:

1)分配对象内存空间:memory = allocate();

2)初始化对象:instance(memory);

3)设置instance指向分配的内存地址:instance = memory;

由于步骤2和步骤3不存在数据依赖关系,因此可能出现执行132步骤的情况。

比如线程1执行了步骤13,还没有执行步骤2,此时instance!=null,但是对象还没有初始化完成;

如果此时线程2抢占到cpu,然后发现instance!=null,然后直接返回使用,就会发现instance为空,就会出现异常。

这就是指令重排可能导致的问题,因此要想保证程序100%正确就需要加volatile禁止指令重排。

3.6 volatile保证禁止指令重排的原理

在3.1中简单介绍了下执行重排的含义,然后通过3.2-3.5,借助单例模式来举例说明多线程情况下,为什么要使用volatile的原因,因为可能存在指令重排导致程序异常。

接下来就介绍下volatile能保证禁止指令重排的原理。

首先要了解一个概念:内存屏障(Memory Barrier),又称为内存栅栏。它是一个CPU指令,有2个作用:

1)保证特定操作的执行顺序;

2)保证某些变量的内存可见性;

由于编译器和处理器都能执行指令重排。如果在指令之间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说,通过插入内存屏障,禁止在内存屏障前后的指令执行重排需优化

内存屏障的另一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本

总结

到此这篇关于Java基础面试题之volatile详解的文章就介绍到这了,更多相关Java volatile详解内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: Java基础面试题之volatile详解

本文链接: https://lsjlt.com/news/151997.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Java基础面试题之volatile详解
    目录1、volatile保证可见性1.1、什么是JMM模型?1.2、volatile保证可见性的代码验证1.2.1、无可见性代码验证1.2.1、volatile保证可见性验证2、vo...
    99+
    2024-04-02
  • 详解Java并发编程基础之volatile
    目录一、volatile的定义和实现原理1、Java并发模型采用的方式2、volatile的定义3、volatile的底层实现原理二、volatile的内存语义1、volatile的...
    99+
    2024-04-02
  • java——《面试题——基础篇》
    全文章节 Java——《面试题——基础篇》 Java——《面试题——JVM篇》 Java——《面试题——多线程&并发篇》 Java——《面试题——Spring篇》 Java——《面试题——SpringBoot篇》 Java—...
    99+
    2023-10-21
    java 面试题
  • java面试中有哪些基础面试题
    这篇“java面试中有哪些基础面试题”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“java面试中有哪些基础面试题”文章吧。作...
    99+
    2023-06-03
  • Java的基础面试题目(4)
    a.hashCode() 有什么用?与 a.equals(b) 有什么关系?hashCode() 方法对应对象整型的 hash 值。它常用于基于 hash 的集合类,如 Hashtable、HashMap、LinkedHashMap等等。它...
    99+
    2020-03-09
    java面试题 Java
  • Java的基础面试题目(3)
    String s = new String("xyz");创建了几个StringObject?是否可以继承String类两个或一个都有可能,”xyz”对应一个对象,这个对象放在字符串常量缓冲区,常量”xyz”不管出现多少遍,都是缓冲区中的那...
    99+
    2018-01-28
    java面试题 Java
  • Java常见的基础面试题
    JDK 和 JRE 有什么区别?JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。JRE:Java Runtime Environment 的简称,java 运行环境,为...
    99+
    2017-11-11
    java面试题 Java
  • Java的基础面试题目(2)
    11、是否可以从一个static方法内部发出对非static方法的调用?不可以。因为非static方法是要与对象关联在一起的,必须创建一个对象后,才可以在该对象上进行方法调用,而static方法调用时不需要创建对象,可以直接调用。 ...
    99+
    2019-02-27
    java面试题 Java
  • Java的基础面试题目(1)
    1、一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制?可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致。 (推荐学习:java面试题目)2、Java有没有goto java中的...
    99+
    2016-11-11
    java面试题 Java
  • Java基础面试题有哪些
    小编给大家分享一下Java基础面试题有哪些,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!面试题1:JDK1.8的新特性有哪些?接口的默认和静态方法:Java 8允...
    99+
    2023-06-20
  • MySQL面试题-基础
    MySQL面试题-基础 MySQL面试题-基础 1、SQL中文全称 结构化查询语言2、SQL语句分类 (1)数据查询语言DQL包括select字段名from表名where条件 (2)...
    99+
    2021-02-19
    MySQL面试题-基础
  • java最新基础知识面试题
    java最新基础知识面试题1、static关键字什么意思?java中是否可以覆盖一个private或者是static的方法?“static”表明一个成员变量或者是成员方法可以在没有所属类的实例变量的情况下访问。java中static方法不能...
    99+
    2014-06-09
    java面试题 java 面试题 基础
  • Java基础概述面试题复习
    目录Java概述何为编程什么是Javajdk1.5之后的三大版本JVM、JRE和JDK的关系JVMJREJDK什么是跨平台性?原理是什么Java语言有哪些特点什么是字节码?采用字节码...
    99+
    2024-04-02
  • Java基础之volatile应用实例分析
    这篇文章主要介绍“Java基础之volatile应用实例分析”,在日常操作中,相信很多人在Java基础之volatile应用实例分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Java基础之volatile...
    99+
    2023-07-02
  • Java基础之StringBuffer详解
    目录一、前言二、用法三、结果四、长度 容量五、IStringBuffer接口六、value和capacity一、前言 StringBuffer是可变长的字符串 1.append 追加...
    99+
    2024-04-02
  • Java基础之Maven详解
    目录一、Maven环境的搭建1. 为什么要学习Maven?2. Maven项目架构管理工具3. 下载安装Maven4. 配置环境变量5. 阿里云...
    99+
    2024-04-02
  • Java基础之TreeMap详解
    目录一、写在前面二、定义三、成员变量四、内部类五、构造器六、成员方法一、写在前面 TreeMap的底层数据结构是红黑树,且TreeMap可以实现集合元素的排序。 所以TreeMap...
    99+
    2024-04-02
  • Java基础之ClassLoader详解
    目录一、ClassLoader简介二、内置的CLassLoader的类型三、BootstrapClassLoader四、ExtClassLoader五、AppClassLoader六...
    99+
    2024-04-02
  • Java基础之FastJson详解
    目录一、fastJson将json格式字符串转化成List集合二、fastJson将json格式字符串转化成对象三、FastJson将对象或集合转化成json格式字符串四、FastJ...
    99+
    2024-04-02
  • MySQL 面试题——MySQL 基础
    目录 1.什么是 MySQL?有什么优点?2.MySQL 中的 DDL 与 DML 是分别指什么?3.✨数据类型 varchar 与 char 有什么区别?4.数据类型 BLOB 与 TEXT...
    99+
    2023-09-20
    mysql 数据库 面试题
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作