请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”

目录

1. 面试官:“请谈谈你对volatile的理解”

2. 面试官接着问:“既然volatile不能保证原子性,那工作中如何解决这个问题呢?”

3. 官试官:“volatile是如何保证多线程环境下的可见性,JAVA内存模型JMM谈谈你对它的了解”

4. 官试官:“你能编写一个volatile修饰的变量在多线程环境可见性示例不?”

5. 面试官:“为什么volatile不保证原子性呀,你知道底层原理么?”

6. 面试官:“你可以编写一下,volatile不保证原子性的解决示例?”

7. 面试官:volatile能禁止指令重排 ,为什么有指令重排,volatile中怎样做到禁止指令重排?

8. 面试官:你在哪些地方用到过volatile?手写一个Volatile单例


1. 面试官:“请谈谈你对volatile的理解”

小李子窃窃私语,这不是正好前段时间复习且项目中使用到的volatile关键字么?于是小李子自信地答道:

volatile是Java虚拟机提供的轻量级的同步机制,volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而进行指令重排序,同时保证多线程环境下变量值变修改后,其他线程可见性;但volatile不保证原子性哦。使用示例:

volatile int number = 0; // 使用volatile 修饰整型变量number,以保证多线程环境下可见性及禁止指令重排。注:volatile不保证原子性
  • 1.1 保证多线程的可见性
  • 1.2 不保证原子性
  • 1.3 禁止指令重排,一般在单例模式下使用较多

2. 面试官接着问:“既然volatile不能保证原子性,那工作中如何解决这个问题呢?”

小李子心想,还来了个连环炮,好在我有准备。于是比利比利地回答:

工作中,我们有两种方式规避这个问题:

1. 使用JDK提供的 Atomic原子类

比如:AtomicInteger来声明变量,是采用了CAS 比较并交换 compareAndSwapInt,底层调用的是native方法,其意思是通过hotspot底层c/c++方法实现。最终实现是调用了 cmpxchg,cmpxchg指令在多线程下也是有可能被打断,所以在加入lock指令 不允许其他线程访问这块内存区域的数据。

2. 加锁,如synchronized 或 ReentrantLock


3. 官试官:“volatile是如何保证多线程环境下的可见性,JAVA内存模型JMM谈谈你对它的了解”

小李子心底阵阵发凉,前面已经回答的很好,还接着问,有完没完!没办法,回忆一下,继续答道:

Java内存模型(Java Memory Model,简称JMM)  本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。

JMM关于同步规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作 (读取赋值等) 必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,线程间的通讯(传值) 必须通过主内存来完成。 

这样子吧,我画一个JMM内存模型图出来看看就清楚了。JMM内存模型图如下:


4. 官试官:“你能编写一个volatile修饰的变量在多线程环境可见性示例不?”

小李子听到这个就想要骂娘了,还得编一个示例。不过,小Case,小李子接过键盘,啪啦啪啦地敲下代码:

package com.java.meet.c01_11_volatile;

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

class MyResource {
    //int number = 0; // 此行代码在多线程环境下,变量修改后不可见
    volatile int number = 0; // 此行代码在多线程环境下,变量修改后可见,因为变量被 volatile修饰

    public void addT060(){
        this.number = 60;
    }

}

public class C_02_Volatile_SeeOK {

    public static void main(String[] args) { // main是一切方法的运行入口
        seeOkByVolatile();
    }

    // volatile 可以保证可见性,及时通知其它线程,主物理内存的值已经被修改。
    // volatile 是通过内存屏障实现
    private static void seeOkByVolatile() {
        MyResource res = new MyResource(); // 资源类

        new Thread(() -> {
            System.out.println(LocalDateTime.now() + "\t" + Thread.currentThread().getName() + "线程\t当前值为:" + res.number);
            // 暂停一会儿线程
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e){  e.printStackTrace();}
            res.addT060();
            System.out.println(LocalDateTime.now() + "\t" + Thread.currentThread().getName() + "线程\t当前值为:" + res.number);
        }, "AAAA").start();

        // 第2个线程就是我们的main线程
        while(res.number == 0){
            // main线程就一直在这里等待循环,走到number值不再等于零
        }
        System.out.println(LocalDateTime.now() + "\t" + Thread.currentThread().getName() + "线程\t当前值为" + res.number);
    }

}

没有被volatile修饰变量,在多线程中被修改值,测试结果如下: 

被volatile修饰变量,在多线程中被修改值,测试结果如下: 

敲出来代码了,哈哈,小李子心里哈哈的暗自高兴~ 


5. 面试官:“为什么volatile不保证原子性呀,你知道底层原理么?”

小李子,捋了下思路,这个So easy嘛。说道:number++在多线程下是非线程安全的。当我们添加volatile 关键字修饰时,n++ 同样被拆分成了3个指令,在多线程环境下,依然不能保证原子性。如下图,是volatile 修饰整型变量n,对n++方法进行反汇编。

package com.java.meet.c01_11_volatile;

/**
 * 6_volatile不保证原子理论解释
 *
 * 使用javap -c或javap -verbose
 */

public class C_06_T1 {

    volatile int n = 0;

    /**
     * MyData.java ====> MyData.class ===> 字节码
     * n++ 被拆分成了3个指令
     * 执行getfield拿到原始n;
     * 执行iadd进行加1操作;
     * 执行putfield写把累加后的值写回
     *
     * 查看字节码,需要配置好External tools
     */

    /**
    /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/bin/javap -c com.java.meet.c01_11_volatile.C_06_T1
    Compiled from "C_06_T1.java"
    public class com.java.meet.c01_11_volatile.C_06_T1 {
        volatile int n;

  public com.java.meet.c01_11_volatile.C_06_T1();
        Code:
        0: aload_0
        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
        4: aload_0
        5: iconst_0
        6: putfield      #2                  // Field n:I
        9: return

        public void add();
        Code:
        0: aload_0
        1: dup
        2: getfield      #2                  // Field n:I
        5: iconst_1
        6: iadd
        7: putfield      #2                  // Field n:I
        10: return
    }

    Process finished with exit code 0
    */
    public void add(){
        n++;
    }
}

6. 面试官:“你可以编写一下,volatile不保证原子性的解决示例?”

小李子胸有成竹的说:这样吧,我演示一个volatile修饰的变量在多线程环境下不保证原子性的及其中一个解决volatile不保证原子性办法。其中,可以通过JDK提供的Atomic原子类来保证原子操作,于是,拿起键盘哒哒地敲起来:

package com.java.meet.c01_11_volatile;

import java.util.concurrent.atomic.AtomicInteger;

class MyNumber {
    volatile int number = 0;

    // 请注意,此时number前面是加了volatile关键字修饰的,volatile不保证原子性
    public void addPlusPlus(){
        number++;
    }

    // 使用JDK提供的原子类,可以解决volatile不保证原子性问题,采用CAS比较并交换
    AtomicInteger atomicInteger = new AtomicInteger();
    public void addMyAtomic(){
        atomicInteger.getAndIncrement();
    }
}

public class C_03_Volatile_NotSupportAtomic {

    public static void main(String[] args) { // main是一切方法的运行入口
        notSupportAtomicByVolatile();
    }

    // volatile 不保证原子性 及 使用JDK Atomic原子类解决volatile不保证原子性问题
    private static void notSupportAtomicByVolatile() {
        MyNumber res = new MyNumber();

        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    res.addPlusPlus();
                    res.addMyAtomic();
                }
            }, String.valueOf(i)).start();
        }

        // 需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值看是多少?
        while (Thread.activeCount() > 2){
            Thread.yield(); // yield我不执行,让其他的线程更好地执行
        }

        System.out.println(Thread.currentThread().getName() + "\t int type,finally number value:" + res.number);
        System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type,finally number value:" + res.atomicInteger);
    }
}

volatile不保证原子性测试结果如下: 


7. 面试官:volatile能禁止指令重排 ,为什么有指令重排,volatile中怎样做到禁止指令重排?

小李子,心里苦呀,我只是想拿月薪30K的薪资,还得挖那么深,没有办法。继续回答:

计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下3种

  1. 编译器优化的重排
  2. 指令并行的重排
  3. 内存系统的重排

单线程环境里面确保程序是最终执行结果和代码顺序执行的结果一致。处理器在进行重新排序是必须要考虑指令之间的数据依赖性

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

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。

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

  1. 一是保证特定操作的执行顺序,
  2. 二是保证某些的内存可见性(利用该特性实现volatile的内存可见性)

由于编译器和处理器都能执行指令重排优化。如果在指令插入一条Memory B 和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

  • 对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存
  • 对volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量


8. 面试官:你在哪些地方用到过volatile?手写一个Volatile单例

小李子喜出望外,这不是送分题。通过DCL双端检查 + Volatile,来实现,卡拉卡拉地写下如下代码:

package com.java.meet.c01_11_volatile;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 单例模式:volatile版本
 *
 */
public class C_10_SingletonDemo {

    /**
     * 多线程环境下,单例模式下,需要DCL机制 + volatile禁止指令重排
     */
    //private static C_10_SingletonDemo instance = null;         // 在非常高的并发情况下,可能获取的对象不是同一个
    private static volatile C_10_SingletonDemo instance = null;   // 在非常高的并发情况下,DCL双端检查 + volatile 可以保证获取的是同一个对象

    // 单例类构造方法私有化
    private C_10_SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t我是构造方法SingletonDemo()");
    }

    // 如果单纯依靠 DCL (Double Check Lock双端检锁机制),还是有问题,可能运行1000万次,才出一次问题
    /**
     * DCL(双端检锁) 机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排
     *
     * 原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化.
     *
     * instance=new SingletonDem(); 可以分为以下步骤(伪代码)
     *
     * memory=allocate(); //1.分配对象内存空间
     *
     * instance(memory);  //2.初始化对象
     *
     * instance=memory;   //3.设置instance的指向刚分配的内存地址,此时instance!=null
     *
     * 步骤2和步骤3不存在数据依赖关系.而且无论重排前还是重排后程序执行的结果在单线程中并没有改变,因此这种重排优化是允许的.
     *
     * memory=allocate();  //1.分配对象内存空间
     *
     * instance=memory;    //3.设置instance的指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完.
     */
    public static C_10_SingletonDemo getInstance(){
        if (instance == null){
            synchronized (C_10_SingletonDemo.class){
                if (instance == null){
                    instance = new C_10_SingletonDemo();
                }
            }
        }

        if (instance == null) {
            System.out.println("----");
        }

        return instance;
    }

    // List本身是不安全的, 使用以下才安全
    public static List<C_10_SingletonDemo> list = Collections.synchronizedList(new ArrayList<C_10_SingletonDemo>());

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                list.add(C_10_SingletonDemo.getInstance());
            }, String.valueOf(i)).start();
        }

        // 等待上述操作完成
        TimeUnit.SECONDS.sleep(2);

        boolean oneSingleObject = true;
        for (int i = 0; i < list.size(); i++) {
            for (int j = i+1; j < list.size(); j++) {
                if (list.get(i) != list.get(j)) {
                    System.out.println("此单例,创建了不同的对象实例!!!\t"  + i + " "+ list.get(i) + "\t" + list.get(j));
                    oneSingleObject = false;
                }
            }
        }

        if (oneSingleObject) {
            System.out.println("通过单例类获取:" + list.size() + " 次依然是同一个对象!");
        }
    }
}

DCL双端检查 + Volatile 单例测试结果如下: 

面试官,看来你对volatile掌握得还不错哈~ 

今天面试,先到这里,回去等通过吧。


文章最后,给大家推荐一些受欢迎的技术博客链接

  1. JAVA相关的深度技术博客链接
  2. Flinak 相关技术博客链接
  3. Spark 核心技术链接
  4. 设计模式 —— 深度技术博客链接
  5. 机器学习 —— 深度技术博客链接
  6. Hadoop相关技术博客链接
  7. 超全干货--Flink思维导图,花了3周左右编写、校对
  8. 深入JAVA 的JVM核心原理解决线上各种故障【附案例】
  9. 请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”
  10. 聊聊RPC通信,经常被问到的一道面试题。源码+笔记,包懂
  11. 深入聊聊Java 垃圾回收机制【附原理图及调优方法】

欢迎扫描下方的二维码或 搜索 公众号“大数据高级架构师”,我们会有更多、且及时的资料推送给您,欢迎多多交流!

                                           

       

 

  • 12
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
-多个线程的几种实现方式包括:承Thread类,实现Runnable接口,实Callable接口,使用线程池。 - Java中的线程池是通过ThreadPoolExecutor类实现的。线程池维护了一个线程队列,可以复用线程,减少线程的创建和销毁开销,提高了性能。 - 不建议直接使用Executors工具类创建线程池是因为它使用的是默认的线程池配置,可能导致线程数量过多,耗尽系统资源。OOM(Out of Memory)是由于创建过多的线程导致内存不足而发生的错误。 - Java内存模型(JMM)是一种规范,定义了多线程程序中各个变量的访问方式。它包括主内存和工作内存,通过控制变量的可见性和原子性来保证线程间的通信与同步。 - 并发编程可能会发生的问题包括:竞态条件、死锁、活锁、饥饿等。可见性问题指一个线程对共享变量的修改对其他线程是否可见,原子性问题指一个操作是否可以被中断或者同时执行。 - 并发编程下会出现原子性问题是因为多个线程同时修改同一个共享变量时,可能会导致不一致的结果。有序性问题是指程序执行的顺序与预期不符。可以使用synchronized关键字、Lock锁等来解决原子性和有序性问题。加上volatile关键字可以保证可见性,禁止指令重排序。 - 内存屏障是通过编译器和处理器来实现的,用于控制指令的执行顺序和内存的可见性。synchronized关键字会在进入和退出临界区时加上内存屏障。 - 单线程指令重排在不影响单线程执行结果的前提下进行优化,但可能会影响多线程的正确性。双重校验锁中使用volatile是为了禁止指令重排,确保多线程环境下的正确性。 - InnoDB的索引是通过B+树实现的。B+树具有树高度低、查询效率高、支持范围查询等优势。 - 聚簇索引与非聚簇索引的区别在于数据的存储方式。聚簇索引将数据行存储在叶子节点中,非聚簇索引则将叶子节点指向数据行。不是所有情况都需要取回表的数据,可以通过覆盖索引来避免回表操作。 - 最左前缀匹配指在使用联合索引时,只有从左到右使用索引的前缀部分才能发挥索引的作用。将区分度高的字段放在最左边可以提高索引的效率。唯一索引与普通索引的区别在于是否允许重复值。 - 排查慢SQL可以通过查看慢查询日志、使用性能分析工具(如EXPLAIN、SHOW PROFILE)、优化查询语句等方法。 - MySQL的锁包括行锁和表锁。行锁在并发性能上更好,但需要更多的系统资源,适合处理并发访问较高的场景。表锁在资源消耗上较少,但并发性能相对较差,适合处理并发访问较低的场景。 - FOR UPDATE语句会对查询到的行加上行锁。 - 悲观锁是指在操作数据时始终假设会发生并发冲突,因此会将数据加锁以阻止其他事务的访问。乐观锁是指不加锁,而是通过版本号或时间戳等机制来判断是否发生冲突,减少了加锁的开销。悲观锁适用于并发冲突较多的场景,乐观锁适用于并发冲突较少的场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不埋雷的探长

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值