美文网首页
Android多线程同步-volatile

Android多线程同步-volatile

作者: Hawozhencai | 来源:发表于2021-04-08 10:30 被阅读0次

本章节内容参考《深入理解Android Java虚拟机ART》进行研究,所参考ART源码版本为 Android 10,如有问题请及时指出;

1. volatile介绍

1.1 volatile定义

  1. volatile是JVM提供的一种最轻量级的同步机制;
  2. Java内存模型(JMM:Java Memory Model)支持的3大特性:(多线程并发访问共享变量时,保证共享变量的)可见性、有序性和原子性;volatile语义用于实现Java内存模型中的可见性和有序性,但不保证原子性;

1.2 可见性

  1. 当线程T对volatile变量a执行写操作后,JMM会把线程T的工作内存中的变量a的新值强制刷新到主内存;同时该写操作还会使其他线程中对应的变量a'无效;
  2. 这样,其他线程想访问自己工作内存中变量a'时发现,a'失效,则会先等待,等待直到主存中变量a的地址更新后,再从主存中重新获取,拿到最新值,实现了线程间的共享变量的可见性;

1.3 有序性

  1. 指被volatile修饰的数据,将会禁止指令重排序优化,进一步解释,指令重排序优化时,不能将volatile变量读/写操作前移/后移;

  2. 简单举例指令重排序,指令重排优化只保证最终结果一致,但不保证中间指令的执行顺序和代码顺序一致,优化的其中一条规则是,没有数据依赖关系,例如:

    // 1. 无依赖关系
    int i = 0;
    boolean b = false;
    i = 1;
    b = true;
    
    // 2. 有依赖关系
    int a = 1;
    int b = 2;
    int c = a * b;
    
  3. 从DCL单例模式理解有序性:

    public class Foo {
        private volatile static Foo foo;
        private Foo() { }
        
        public static Foo getFoo() {
            // (1)
            if (foo == null) {
                // (2)
                synchronized (Foo.class) {
                    // (3)
                    if (foo == null) {
                        // (4)
                        foo = new Foo();
                    }
                }
            }
            return foo;
        }
    }
    
    • 多线程获取单例时,如果foo对象没有被volatile修饰,则有可能造成获取单例失败(foo对象非法),为什么?
    • 在(4)步骤中,虽然Java代码是一行,去初始化一个foo对象,但运行时会分解成3步:1)在堆中为Foo对象开辟内存空间;2)调用Foo类的构造函数,初始化赋值成员变量;3)调用"=",将引用foo指向分配的堆内存;此时foo就非空了;
    • 由于JVM执行重排序优化,所以无法保证 2)和 3)的先后顺序;
    • 如果已经获得锁的线程A正在初始化foo对象且走到 3),但 3)在 2)前面执行了(即以 1)-> 3)-> 2)的顺序初始化foo对象),此时foo引用就不为null了,这时候如果线程B想要获取单例,在(1)发现foo!=null成立,则会直接返回foo对象给线程B,但此时的foo对象是非法的、不完整的;
    • 这就是为什么DCL必须加volatile的原因;

1.4 内存屏障

  • 本节引入内存屏障的概念;
  • 引入内存屏障的目的是什么?本章只针对Android的Java虚拟机ART ARM架构进行讨论,其他JVM的内存屏障实现区别较大,不细分析。ARM架构是弱内存序的,即CPU访问内存的顺序不一定按我们的预期,也就是说,CPU会打着"提高效率的旗号",忽略了我们代码的上下文关系。这种弱内存序模型的架构,效率会提高,但在多线程场景中就会导致最终结果不正确,这一点在前面1.3节中解释过了,所以引入内存屏障,可以让开发人员在某些时候"对抗"这种指令重排序优化;
  • 下面分别介绍 HotSpot JVM(水很深,如有兴趣可自行研究)、ART中对内存屏障的定义,不同的平台内存屏障的实现是不同的:

1.4.1 HotSpot JVM 的内存屏障

  1. 对于 HotSpot JVM,介绍了基于 JSR-133规范 定义的4种基本内存屏障指令:
    • LoadLoad:例如指令执行顺序为:1)Load1;2)LoadLoad;3)Load2;Load1操作必须在Load2操作前被执行,并且Load1也不会被指令重排序优化到Load2之后执行;
    • StoreStore:例如指令执行顺序为:1)Store1;2)StoreStore;3)Store2;Store1操作必须在Store2操作前被执行,同时Store1写入的数据会被写回到主存中,并通知其他线程该数据的备份失效,这样其他线程在访问自己备份数据的时候发现失效,则重新从主存中获取。此外Store2操作也不会被指令重排序优化到Store1之前;
    • LoadStore:类推;
    • StoreLoad:类推;
  2. 但是不同的CPU架构,具体的实现不一样,不是说所有的CPU都会用到这4种内存屏障,比如x86 下使用的是 lock 来实现 StoreLoad,并且只有 StoreLoad 有效果,举个简单的例子,volatile a:
    Load a
    Load a.field
    
    像这种访问变量a,接着访问a的成员变量,CPU可以保证两次读操作的顺序,那么两次读操作之间就不需要LoadLoad内存屏障,在JVM将字节码生成汇编指令时优化掉,替换成nop空操作;

1.4.2 ART ARM架构 的内存屏障

  1. ARM架构内存屏障的具体实现和 HotSpot JVM x86 不同;
  2. 内存屏障在执行的不同时期,可以分为编译器内存屏障和CPU内存屏障,前者并不能解决弱内存序引入的问题,只能保证编译后动作1在动作2之前,而CPU内存屏障是真正用来解决问题的;
  3. ARM架构提供了3种CPU内存屏障:
    • DMB:Data Memory Barrier,数据内存屏障,仅当所有在它前面的内存访问都执行完毕后,才执行它后面的内存访问动作(注意只对内存访问敏感),其它非内存访问指令依然可以乱序执行;目前Android虚拟机中的volatile实现,只看到用到这个指令;
    • DSB:Data Synchronous Barrier,数据同步屏障,比DMB严格:仅当所有在它前面的内存访问都执行完毕后,才执行它在后面的指令(任何指令都要等待);
    • ISB:Instruction Synchronous Barrier,指令同步屏障,该指令将刷新buffer,ISB之后的指令需要重新从memory(可以理解为主存)取值,以保证所有它前面的指令都执行完毕之后,才执行它后面的指令;

2. volatile的实现

  • 下面将举例说明ART是如何实现使用内存屏障访问volatile变量,来保证有序性和可见性,由于目前常用的ART虚拟机默认是机器码执行模式,下面都是基于这种模式介绍;
  • 按照分析synchronized的套路继续分析volatile:Java层实现字节码实现虚拟机实现汇编指令实现

2.1 Java层实现

  • Java层代码如下所示:
    public class VolatileFoo {
        private volatile boolean flag;
    
        public void t4() {
            // 1. 读取flag
            if (flag) {
                // 2. 写入flag
                flag = true;
            }
        }
    }
    

2.2 字节码实现

  • 继续查看反编译后的VolatileFoo.class文件:
    private volatile boolean flag;
      descriptor: Z
      flags: ACC_PRIVATE, ACC_VOLATILE
    
  • 在Java类中,类及成员变量、成员函数都有访问修饰符,这些访问控制信息会转换为对应的access_flags,在字节码中,被volatile修饰的成员变量会增加ACC_VOLATILE标记,后续会根据这个标记位和CPU特性,在读/写成员变量的前后,分别生成内存屏障指令,来保证读/写成员变量时的有序性;
  • 【机器码执行模式下】,想 读/写 某个成员变量时,会使用Java指令中的 iget-XX/iput-XX,使用apktool工具可以查看apk反编译出来的smali文件,该文件是将dex文件解析成方便阅读的格式:
    apktool d -f app/build/outputs/apk/debug/app-debug.apk -o ./oat/
    .class Lcom/android/myapplication/VolatileFoo;
    .super Ljava/lang/Object;
    .source "VolatileFoo.java"
    
    # instance fields
    .field private volatile flag:Z
    
    ...
    
    # virtual methods
    .method public t4()V
        .locals 1
    
        .line 10
        // 读操作的Java指令
        iget-boolean v0, p0, Lcom/android/myapplication/VolatileFoo;->flag:Z
    
        if-eqz v0, :cond_0
    
        .line 11
        const/4 v0, 0x1
        // 写操作的Java指令
        iput-boolean v0, p0, Lcom/android/myapplication/VolatileFoo;->flag:Z
    
        .line 13
        :cond_0
        return-void
    .end method
    

2.3 Android虚拟机实现

  • iget-XX/iput-XX Java指令会先编译成汇编指令,如果成员变量被volatile修饰,ARM架构下会添加一条内存屏障指令,以iget为例,ARM平台编译iget指令时,会在iget后生成一条内存屏障指令,目的是保证执行iget操作获取某成员变量的操作不会被虚拟机指令重排序优化放到内存屏障指令后执行,这样来实现内存顺序一致的要求。iput-XX指令会在写操作的前后各插入一条内存屏障指令。下面看下iget指令和iput指令生成对应内存屏障指令的过程,在介绍synchronized时知道,ARM平台编译iget指令的函数在InstructionCodeGeneratorARMVIXL::HandleFieldGet(在art/compiler/optimizing/code_generator_arm_vixl.cc文件中):
    void InstructionCodeGeneratorARMVIXL::HandleFieldGet(HInstruction* instruction,
                                                         const FieldInfo& field_info) {
      ...
      bool is_volatile = field_info.IsVolatile();
      ...
    
      switch (load_type) {
        case DataType::Type::kBool:
        case DataType::Type::kUint8:
        case DataType::Type::kInt8:
        case DataType::Type::kUint16:
        case DataType::Type::kInt16:
        case DataType::Type::kInt32: {
          ...
          break;
        }
      
      ...
    
      if (is_volatile) {
        if (load_type == DataType::Type::kReference) {
          ...
        } else {
          // 对于示例代码中读取boolean类型的成员变量,最终会走到这里,生成内存屏障指令
          codegen_->GenerateMemoryBarrier(MemBarrierKind::kLoadAny);
        }
      }
    }  
    
    // 继续看GenerateMemoryBarrier()函数
    void CodeGeneratorARMVIXL::GenerateMemoryBarrier(MemBarrierKind kind) {
      DmbOptions flavor = DmbOptions::ISH;  // Quiet C++ warnings.
      switch (kind) {
        case MemBarrierKind::kAnyStore:
        // 传入kLoadAny类型,最终flavor赋值为ISH
        case MemBarrierKind::kLoadAny:
        case MemBarrierKind::kAnyAny: {
          flavor = DmbOptions::ISH;
          break;
        }
        case MemBarrierKind::kStoreStore: {
          flavor = DmbOptions::ISHST;
          break;
        }
        default:
      }
      // "__ "替换为获取对应汇编器的方法"assembler->GetVIXLAssembler()->"
      // dmb指令只会影响内存访问指令的顺序,保证在此指令前的内存访问完成后才执行后面的内存访问指令
      __ Dmb(flavor);
    }
    

下面是写操作时生成内存屏障指令的实现:

void InstructionCodeGeneratorARMVIXL::HandleFieldSet(HInstruction* instruction,
                                                     const FieldInfo& field_info,
                                                     bool value_can_be_null) {
  ...
  bool is_volatile = field_info.IsVolatile();
  ...

  // 写操作,会生成2条内存屏障指令,这是在写操作之前的一条
  if (is_volatile) {
    codegen_->GenerateMemoryBarrier(MemBarrierKind::kAnyStore);
  }

  switch (field_type) {
    case DataType::Type::kBool:
    case DataType::Type::kUint8:
    case DataType::Type::kInt8:
    case DataType::Type::kUint16:
    case DataType::Type::kInt16:
    case DataType::Type::kInt32: {
      StoreOperandType operand_type = GetStoreOperandType(field_type);
      // 写操作
      GetAssembler()->StoreToOffset(operand_type, RegisterFrom(value), base, offset);
      break;
    }
    // 其他case
    ...
  }
  ...
  // 写操作后,加入第2条内存屏障指令
  if (is_volatile) {
    codegen_->GenerateMemoryBarrier(MemBarrierKind::kAnyAny);
  }
}
  • 可以看到,最终生成了dmb汇编指令(和Hotspot虚拟机生成的指令不同),并且传入了ISH参数;

2.4 汇编指令实现

  • 接着验证是否真的生成dmb指令?需要查看.oat文件;

  • ART虚拟机并不是直接执行dex文件,而是执行优化好的二进制代码(放在.oat文件中),下面是如何编译查看.oat文件的流程:

    javac VolatileFoo.java
    // 用D8编译器将.class文件编译成.dex文件
    java -jar ~/Library/Android/sdk/build-tools/28.0.3/lib/d8.jar --release --output ./ src/juc/VolatileFoo.class
    adb push classes.dex /sdcard/download/
    adb shell
    cd sdcard/download/
    dex2oat --dex-file=./classes.dex --oat-file=./classes.oat  // 使用手机dex2oat工具进行优化
    oatdump --oat-file=./classes.oat  // terminal中显示.oat内容
    
  • 具体内容如下所示,截取部分重要内容:

    1: void juc.VolatileFoo.t4() (dex_method_idx=2)
    DEX CODE:
      0x0000: 5510 0000                 | iget-boolean v0, v1, Z juc.VolatileFoo.flag // field@0
      0x0002: 3800 0500                 | if-eqz v0, +5
      0x0004: 1210                      | const/4 v0, #+1
      0x0005: 5c10 0000                 | iput-boolean v0, v1, Z juc.VolatileFoo.flag // field@0
      0x0007: 0e00                      | return-void
    OatMethodOffsets (offset=0x0000081c)
      code_offset: 0x00001039 
    OatQuickMethodHeader (offset=0x00001020)
      vmap_table: (offset=0x00000818)
        Optimized CodeInfo (number_of_dex_registers=2, number_of_stack_maps=0)
          StackMapEncoding (native_pc_bit_offset=0, dex_pc_bit_offset=0, dex_register_map_bit_offset=1, inline_info_bit_offset=1, register_mask_bit_offset=1, stack_mask_index_bit_offset=1, total_bit_size=1)
          DexRegisterLocationCatalog (number_of_entries=0, size_in_bytes=0)
    QuickMethodFrameInfo
      frame_size_in_bytes: 0
      core_spill_mask: 0x00004020 (r5, r14)
      fp_spill_mask: 0x00000000 
      vr_stack_locations:
        locals: v0[sp + #4294967280]
        ins: v1[sp + #4]
        method*: v2[sp + #0]
    CODE: (code_offset=0x00001039 size_offset=0x00001034 size=26)...
      0x00001038: 7a08          ldrb r0, [r1, #8]
      0x0000103a: f3bf8f5b      dmb ish  // 此处是读取flag变量后,插入保证有序性的指令
      0x0000103e: 2800          cmp r0, #0
      0x00001040: f0008006      beq.w 0x00001050
      0x00001044: 2001          movs r0, #1
      0x00001046: f3bf8f5b      dmb ish  // 此处是写flag变量前,插入保证有序性的指令
      0x0000104a: 7208          strb r0, [r1, #8] // strb:字节数据存储指令
      0x0000104c: f3bf8f5b      dmb ish
      0x00001050: 4770          bx lr
    
  • 从CODE部分可以找到,确实有 dmb ish 指令;

  • dmb指令是什么?查看ARM官方解释:DMB官方解释

    【Data Memory Barrier】 acts as a memory barrier. It ensures that all explicit memory accesses that appear in program order before the DMB instruction are observed before any explicit memory accesses that appear in program order after the DMB instruction. It does not affect the ordering of any other instructions executing on the processor.

    1. dmb 是一个数据内存屏障;
    2. dmb 仅当所有在它前面的内存访问操作都执行完毕后,才提交(commit)在它后面的内存访问操作;即,确保当前程序【访问内存的指令(load、store)】执行顺序不会有重排序,且不影响其他指令,注意,dmb针对的都是内存访问操作,不针对非内存访问操作;
  • ISH(Inner Shareable)参数又代表什么?表示访问内部共享区域,也就是说dmb之后的指令(此处指的就是strb指令)会操作共享区域的数据(从JVM层面看,就是访问堆内存中的数据、Java代码中的成员变量);

  • 总结:ARM平台,【通过 DMB指令+ISH参数 实现volatile语义】;

    备注:x86平台通过"lock addl"指令实现volatile语义,有兴趣可以自己研究一下;

3. 参考:

JSR-133
Java核心技术 卷I
深入理解Android Java虚拟机ART
Java volatile深入解析
一次深入骨髓的 volatile 研究
volatile底层原理详解
ARM Developer

相关文章

  • Android多线程同步-volatile

    本章节内容参考《深入理解Android Java虚拟机ART》进行研究,所参考ART源码版本为 Android 1...

  • volatile的作用

    Volatile的介绍: 使用volatile的原因: 用在多线程,目的同步变量 Volatile变量相对于锁更简...

  • 2.安全性

    java中多线程同步包括: synchronized 显示锁 volatile 原子变量 之所以要使用同步,是因为...

  • 20170206-多线程同步volatile与Condition

    多线程同步 201612 volatile 对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程...

  • 多线程下volatile 关键字的作用

    Volatile 关键字 作用:Volatile 关键字是多线程下,最小轻量级的同步机制。过程:首先Volatil...

  • Java 面试题精选(一)

    1,volatile关键字是否能保证线程安全?() 答案:否 volatile关键字用在多线程同步中,可保证读取的...

  • Java笔试题库

    1,volatile关键字是否能保证线程安全?() 答案:否 volatile关键字用在多线程同步中,可保证读取的...

  • java程序员必须知道的内存知识-应用层

    1.volatile 可见性,使用volatile修饰的变量可以立刻被其它线程读取到,经常会被用到多线程同步的关键...

  • 2019 Java 底层面试题上半场(第一篇)

    JUC多线程及高并发 请谈谈你对volatile的理解 volatile是Java虚拟机提供的轻量级的同步机制 ...

  • Android 中的同步

    From: [Android教程] 浅谈Java同步锁(Android中的同步) 多线程应用中,我们往往会对同一对...

网友评论

      本文标题:Android多线程同步-volatile

      本文链接:https://www.haomeiwen.com/subject/bjmmkltx.html