阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试

简介: 阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试

一、引言

对于Java开发者而言,关于底层知识,我们一般当做黑盒来进行使用,不需要去打开这个黑盒。

但随着目前程序员行业的发展,我们有必要打开这个黑盒,去探索其中的奥妙。

本篇系列文章,将带你一起探索底层黑盒的奥秘之处。

阅读本篇文章之前,建议下载 openJDK 效果会更好

openJDK下载地址:openJDK

如果感觉网速较慢,建议关注公众号:爱敲代码的小黄,发送:openJDK 即可获取百度网盘链接。

大家可不可以给我点个关注呀~

二、操作系统

1、CPU的乱序执行

CPU在进行读等待的同时执行指令,是CPU乱序的根源,不是乱,而是提高效率

我们来看下面这个程序:

x = 0;
y = 0;
a = 0;
b = 0;
Thread one = new Thread(new Runnable() {
    public void run() {
        //由于线程one先启动,下面这句话让它等一等线程two. 可根据自己电脑的实际性能适当调整等待时间.
        //shortWait(100000);
        a = 1;
        x = b;
    }
});
Thread other = new Thread(new Runnable() {
    public void run() {
        b = 1;
        y = a;
    }
});
one.start();
other.start();
one.join();
other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if (x == 0 && y == 0) {
    System.err.println(result);
    break;
}

我们可以看到,如果我们的CPU没有乱序执行的话,那么 a = 1 必然在 x = b 前面,b = 1 必然在 y = a 的前面

我们可以得到什么结论,也就是 xy 肯定不能同时为 0 (这里读者可以好好想一想,为什么不能同时为0)

我们运行下程序,得到如下结果:

我们在运行 2728842 次的时候,得到了该结果,验证了我们的结论。

2.1 乱序可能会出现的问题

常见例子:DCL为什么要加 volatile

我们以下面举例:

class T{
  int m = 8;
}
T t = new T();

反编译汇编码:

0 new #2 <T>
3 dup
4 invokespeecial # 3 <T.<init>>
7 astore_1
8 return 

我们对于汇编码逐步分析:

  • new #2 <T>:创建 m = 0 的对象并且栈帧中有一个引用指向该对象
  • dup:在我们的栈帧中复制一份引用
  • invokespecial #3 <T.<init>>:弹出一个栈帧中的值,实例化他的构造方法
  • astore_1:将我们栈帧的引用赋值给 t,这里 1 指的是我们本地变量表中的第一位

    所以,我们想一个事情,上面我们已经证明CPU存在乱序的现象,那么会对我们的上述操作有什么危害呢?
  • 当我们的 astore_1 在我们的 invokespeecial # 3 <T.<init>> 执行前执行,会导致我们的将我们没有实例化的对象赋值给 t,如下如所示:

所以为了避免这种现象,我们要对 DCLvolatile

问题来了,我们 volatile 是怎么保证有序性的呢?

2.2 如何禁止指令重排序

我们对于禁止指令重排序,从以下三方面来谈:

  • 代码层面
  • 字节码层面
  • JVM层面
  • CPU层面

2.2.1 Java 代码层面

  • 直接加一个 volatile 关键字即可
public class TestVolatile {
    public static volatile int counter = 1;
    public static void main(String[] args) {
        counter = 2;
        System.out.println(counter);
    }
}

2.2.1 字节码层面

在字节码层面,当对 volatile 进行反编译后,我们可以看到 VCC_volatile

我们对上述代码进行反编译,得到其 字节码

通过 javac TestVolatile.java 将类编译为class文件,再通过 javap -v TestVolatile.class 命令反编译查看字节码文件

这里我们只展示这段代码得字节码:public static volatile int counter = 1;

  public static volatile int counter;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE
    // 下面为初始化counter时的字节码
    0: iconst_2
    1: putstatic     #2                  // Field counter:I
    4: getstatic     #3                  // Field 
  • descriptor:代表方法参数和返回值
  • flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE:标志
  • putstatic:对静态属性进行操作

我们后续的操作都可以通过 ACC_VOLATILE 这个标志来知道该变量已被 volatile 所修饰

2.2.3 HotSpot 源码层面

对于带有 volatile 修饰的变量,我们的 JVM 是怎么去实现的呢?

通常我们在我网站上会看到这四个词语:StoreStoreStoreLoadLoadStoreLoadLoad

我们的 JVM 确实是这样实现的,我们一起来看一下具体的实现吧。

Java中,静态属性属于类的。操作静态属性,对应的指令为 putstatic

我们以 openjdk8 根路径 jdk\src\hotspot\share\interpreter\zero 路径下的

bytecodeInterpreter.cpp 文件中,处理 putstatic 指令的代码:

CASE(_putstatic):
    {
          // .... 省略若干行 
          // Now store the result 现在要开始存储结果了
          // ConstantPoolCacheEntry* cache;     -- cache是常量池缓存实例
          // cache->is_volatile()               -- 判断是否有volatile访问标志修饰
          int field_offset = cache->f2_as_index();
          // ****重点判断逻辑**** 
          if (cache->is_volatile()) { 
            // volatile变量的赋值逻辑
            if (tos_type == itos) {
              obj->release_int_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == atos) {// 对象类型赋值
              VERIFY_OOP(STACK_OBJECT(-1));
              obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
              OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
            } else if (tos_type == btos) {// byte类型赋值
              obj->release_byte_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ltos) {// long类型赋值
              obj->release_long_field_put(field_offset, STACK_LONG(-1));
            } else if (tos_type == ctos) {// char类型赋值
              obj->release_char_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == stos) {// short类型赋值
              obj->release_short_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ftos) {// float类型赋值
              obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
            } else {// double类型赋值
              obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
            }
            // *** 写完值后的storeload屏障 ***
            OrderAccess::storeload();
          } else {
            // 非volatile变量的赋值逻辑
          }       
  }

这里贴一下 cache->is_volatile() 的源码,路径:jdk\src\hotspot\share\utilities\accessFlags.hpp

  // Java access flags
  bool is_public      () const         { return (_flags & JVM_ACC_PUBLIC      ) != 0; }
  bool is_private     () const         { return (_flags & JVM_ACC_PRIVATE     ) != 0; }
  bool is_protected   () const         { return (_flags & JVM_ACC_PROTECTED   ) != 0; }
  bool is_static      () const         { return (_flags & JVM_ACC_STATIC      ) != 0; }
  bool is_final       () const         { return (_flags & JVM_ACC_FINAL       ) != 0; }
  bool is_synchronized() const         { return (_flags & JVM_ACC_SYNCHRONIZED) != 0; }
  bool is_super       () const         { return (_flags & JVM_ACC_SUPER       ) != 0; }
  bool is_volatile    () const         { return (_flags & JVM_ACC_VOLATILE    ) != 0; }
  bool is_transient   () const         { return (_flags & JVM_ACC_TRANSIENT   ) != 0; }
  bool is_native      () const         { return (_flags & JVM_ACC_NATIVE      ) != 0; }
  bool is_interface   () const         { return (_flags & JVM_ACC_INTERFACE   ) != 0; }
  bool is_abstract    () const         { return (_flags & JVM_ACC_ABSTRACT    ) != 0; }

我们看一下赋值 obj->release_long_field_put(field_offset, STACK_LONG(-1)) 的源代码:jdk\src\hotspot\share\oops\oop.inline.hpp

jlong oopDesc::long_field_acquire(int offset) const                   { return Atomic::load_acquire(field_addr<jlong>(offset)); }
void oopDesc::release_long_field_put(int offset, jlong value)         { Atomic::release_store(field_addr<jlong>(offset), value); }

我们前往 jdk\src\hotspot\share\runtime\atomic.hpp看一下 Atomic::release_store 的方法

inline T Atomic::load_acquire(const volatile T* p) {
  return LoadImpl<T, PlatformOrderedLoad<sizeof(T), X_ACQUIRE> >()(p);
}
template <typename D, typename T>
inline void Atomic::release_store(volatile D* p, T v) {
  StoreImpl<D, T, PlatformOrderedStore<sizeof(D), RELEASE_X> >()(p, v);
}

我们可以清楚的看到,const volatile T* pvolatile D* p 在调用的时候,直接使用了 C/C++volatile 关键字

我们继续往下看,在我门执行完参数的赋值后,会有这个一个操作:OrderAccess::storeload();

我们观察 jdk\src\hotspot\share\runtimeorderAccess.hpp 文件,发现有这么一段代码

// barriers 屏障
  static void     loadload();
  static void     storestore();
  static void     loadstore();
  static void     storeload();
  static void     acquire();
  static void     release();
  static void     fence();

我们可以清楚的看到,这就是我们在各大网站看到的 JVM 的读写屏障

当然,我们还要看其在 linux_x86 实现方式,在 jdk\src\hotspot\os_cpu\linux_x86orderAccess_linux_x86.hpp

// A compiler barrier, forcing the C++ compiler to invalidate all memory assumptions
static inline void compiler_barrier() {
  __asm__ volatile ("" : : : "memory");
}
inline void OrderAccess::loadload()   { compiler_barrier(); }
inline void OrderAccess::storestore() { compiler_barrier(); }
inline void OrderAccess::loadstore()  { compiler_barrier(); }
inline void OrderAccess::storeload()  { fence();            }
inline void OrderAccess::acquire()    { compiler_barrier(); }
inline void OrderAccess::release()    { compiler_barrier(); }

2.2.4 CPU层面

  • Intel 的原语指令:mfence内存屏障ifence读屏障sfence写屏障

我们可以看到,最关键的是这一行代码:__asm__ volatile ("" : : : "memory");

  • __asm__ :用于指示编译器在此插入汇编语句
  • volatile :告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。

("" : : : "memory"):memory 强制 gcc 编译器假设 RAM 所有内存单元均被汇编指令修改,这样 cpu 中的 registers 和 cache 中已缓存的内存单元中的数据将作废。

  • cpu 将不得不在需要的时候重新读取内存中的数据。这就阻止了 cpu 又将 registers, cache 中的数据用于去优化指令,而避免去访问内存。

简单概括:告诉我们的CPU,别瞎几把给我优化了,我就要串行执行。

这样我们可以看到,这些指令都是通过更改CPU的 寄存器 和 缓存 来保持有序性的

到这基本面试就差不多了,能打败 80% 面试者和面试官了,但我们这篇文章还不够!

我们观察这些方法,会发现有一个叫 fence() 的方法,我们观察一下这个方法:

inline void OrderAccess::fence() {
   // always use locked addl since mfence is sometimes expensive
   // 
#ifdef AMD64
  __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
  __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  compiler_barrier();
}

我们可以看到,我们的方法不建议我们使用我们的原语指令 mfence(内存屏障) ,因为 mfence 的资源消耗要比 locked 资源消耗的多

直接判断是不是 AMD64 来对其不同的寄存器 rsp\esp做处理

"lock; addl $0,0(%%rsp)":在 rsp 寄存器上加一个 0) 指令是一个 Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个 CPU

到这里,我们的 volatile 基本差不多了,应该可以通过打败 90% 的面试官了

2.3 hanppens-before原则

简单来说,JVM规定重排序必须遵守的规则(了解即可

  • 程序次序规则
  • 管程锁定规则
  • volatile
  • 线程启动规则
  • 线程终止规则
  • 线程中断规则
  • 对象终结规则
  • 传递性

2.4 as if serial

不管如何重排序,单线程执行的结果不会改变

三、总结

这一篇文章写了大概一个星期,最难的地方在于一直找不到一个从浅入深的过程,导致自己一直不知道该怎么写

最终还是成功的完成了,让自己对于 volatile 的理解又进了一步

至少看完这篇文章,在 volatile 的问题上,不惧怕任何面试官








相关文章
|
24天前
|
存储 关系型数据库 MySQL
阿里面试:为什么要索引?什么是MySQL索引?底层结构是什么?
尼恩是一位资深架构师,他在自己的读者交流群中分享了关于MySQL索引的重要知识点。索引是帮助MySQL高效获取数据的数据结构,主要作用包括显著提升查询速度、降低磁盘I/O次数、优化排序与分组操作以及提升复杂查询的性能。MySQL支持多种索引类型,如主键索引、唯一索引、普通索引、全文索引和空间数据索引。索引的底层数据结构主要是B+树,它能够有效支持范围查询和顺序遍历,同时保持高效的插入、删除和查找性能。尼恩还强调了索引的优缺点,并提供了多个面试题及其解答,帮助读者在面试中脱颖而出。相关资料可在公众号【技术自由圈】获取。
|
4天前
|
SQL 关系型数据库 MySQL
阿里面试:1000万级大表, 如何 加索引?
45岁老架构师尼恩在其读者交流群中分享了如何在生产环境中给大表加索引的方法。文章详细介绍了两种索引构建方式:在线模式(Online DDL)和离线模式(Offline DDL),并深入探讨了 MySQL 5.6.7 之前的“影子策略”和 pt-online-schema-change 方案,以及 MySQL 5.6.7 之后的内部 Online DDL 特性。通过这些方法,可以有效地减少 DDL 操作对业务的影响,确保数据的一致性和完整性。尼恩还提供了大量面试题和解决方案,帮助读者在面试中充分展示技术实力。
|
6天前
|
存储 缓存 Java
大厂面试高频:Volatile 的实现原理 ( 图文详解 )
本文详解Volatile的实现原理(大厂面试高频,建议收藏),涵盖Java内存模型、可见性和有序性,以及Volatile的工作机制和源码案例。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:Volatile 的实现原理 ( 图文详解 )
|
1月前
|
消息中间件 存储 canal
阿里面试:canal+MQ,会有乱序的问题吗?
本文详细探讨了在阿里面试中常见的问题——“canal+MQ,会有乱序的问题吗?”以及如何保证RocketMQ消息有序。文章首先介绍了消息有序的基本概念,包括全局有序和局部有序,并分析了RocketMQ中实现消息有序的方法。接着,针对canal+MQ的场景,讨论了如何通过配置`canal.mq.partitionsNum`和`canal.mq.partitionHash`来保证数据同步的有序性。最后,提供了多个与MQ相关的面试题及解决方案,帮助读者更好地准备面试,提升技术水平。
阿里面试:canal+MQ,会有乱序的问题吗?
|
27天前
|
消息中间件 架构师 Java
阿里面试:秒杀的分布式事务, 是如何设计的?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试阿里、滴滴、极兔等一线互联网企业时,遇到了许多关于分布式事务的重要面试题。为了帮助大家更好地应对这些面试题,尼恩进行了系统化的梳理,详细介绍了Seata和RocketMQ事务消息的结合,以及如何实现强弱结合型事务。文章还提供了分布式事务的标准面试答案,并推荐了《尼恩Java面试宝典PDF》等资源,帮助大家在面试中脱颖而出。
|
1月前
|
SQL 关系型数据库 MySQL
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
尼恩,一位40岁的资深架构师,通过其丰富的经验和深厚的技術功底,为众多读者提供了宝贵的面试指导和技术分享。在他的读者交流群中,许多小伙伴获得了来自一线互联网企业的面试机会,并成功应对了诸如事务ACID特性实现、MVCC等相关面试题。尼恩特别整理了这些常见面试题的系统化解答,形成了《MVCC 学习圣经:一次穿透MYSQL MVCC》PDF文档,旨在帮助大家在面试中展示出扎实的技术功底,提高面试成功率。此外,他还编写了《尼恩Java面试宝典》等资料,涵盖了大量面试题和答案,帮助读者全面提升技术面试的表现。这些资料不仅内容详实,而且持续更新,是求职者备战技术面试的宝贵资源。
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
|
1月前
|
Kubernetes 架构师 算法
阿里面试:全国14亿人,统计出重名最多的前100个姓名
文章介绍了如何解决“从全国14亿人的数据中统计出重名人数最多的前100位姓名”的面试题,详细分析了多种数据结构的优缺点,最终推荐使用前缀树(Trie)+小顶堆的组合。文章还提供了具体的Java代码实现,并讨论了在内存受限情况下的解决方案,强调了TOP N问题的典型解题思路。最后,鼓励读者通过系统化学习《尼恩Java面试宝典》提升面试技巧。
阿里面试:全国14亿人,统计出重名最多的前100个姓名
|
1月前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
3月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
8天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?