Android工程师学习JVM(九)-Java内存模型和volatile关键字

678 阅读13分钟

前言

在学习JVM这个系列文章中,已经讲解了JVM规范、Class文件格式以及如何阅读字节码、ASM字节码处理、类的生命周期及自定义类加载器、内存分配、字节码执行引擎、垃圾回收机制等。本篇介绍HotSpot中的垃圾收集器,对于理解垃圾回收机制会很有帮助

如果你对JVM、字节码、Class文件格式、ASM字节码处理、类加载及自定义类加载器、内存分配、字节码执行引擎、垃圾回收机制、垃圾收集器有兴趣的话,可以看之前的文章哈,相信会收获更多哦

Android工程师学习JVM(八)-HotSpot中的垃圾收集器

Android工程师学习JVM(七)-面试常考之垃圾回收

Android工程师学习JVM(六)-字节码执行引擎

Android工程师学习JVM(五)-内存分配基础知识

Android工程师学习JVM(四)-类加载、连接、初始化、卸载

Android工程师学习JVM(三)-字节码框架ASM使用

Android工程师学习JVM(二)-教你阅读Java字节码

Android工程师学习JVM(一)-JVM概述

1、Java内存模型和内存间的交互操作

1.1、Java内存模型

Java内存模型(Java Memory Model)是Java虚拟机规范定义的,用来屏蔽java程序在不同硬件和不同操作系统上运作时对内存访问的差异,目的是实现java程序在各种不同平台上都能达到内存访问的一致性。避免像c/c++程序可能出现的在Windows平台运行正常,在linux平台却运行异常的情况

Java内存模型的主要目标是定义程序中变量的访问规则,即在虚拟机中将变量存储到主内存或将变量从主内存中取出这样的底层细节。示意图如下:

所有变量(共享的)都存储在主内存中,每个线程都有自己的工作内存;工作内存中保存该线程使用到的变量的副本拷贝

注意这里的变量跟我们写java程序中的变量不完全等同。这里的变量是指实例字段,静态字段,构成数组对象的元素,但不包括局部变量和方法参数(因为这是线程私有的)。

可以简单的认为主内存是java虚拟机内存区域中的堆。而局部变量和方法参数是在虚拟机栈中定义的,线程私有的,处于工作内存中。如果堆中的变量在多个线程中都使用到了,因为工作内存中使用到的是变量的副本拷贝,就涉及到了堆和不同虚拟机栈中的变量的值的一致性问题了

虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能互相访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递

注意:主内存、工作内存与java内存区域中的java堆,虚拟机栈,方法区并不是一个层次的内存划分,两者基本上是没有关系的,只是为了便于理解,做的类比

1.2、内存之间的交互

Java内存中线程的工作内存和主内存交互是有java虚拟机定义的8种操作来完成的,每种操作必须是原子性的

java虚拟机中主内存和工作内存的交互,就是一个变量如何从主内存传输到工作内存中,如何把修改后的变量从工作内存同步回主内存。下面看下示意图:

lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线成独占这个变量

unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定

read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用

load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)

use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作

assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作

store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用

write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中

1.3、内存之间交互的规则

要把一个变量从主内存传输到工作内存,那就要顺序的执行read和load操作。

要把一个变量从工作内存回写到主内存,就要顺序的执行store和write操作。

对于普通变量,虚拟机只是要求顺序的执行,并没有要求连续的执行,所以如下也是正确的。对于两个线程,分别从主内存中读取变量a和b的值,并不一样要read a; load a; read b; load b; 也会出现如下执行顺序:read a; read b; load b; load a; (对于volatile修饰的变量会有一些其他规则,后边会详细列出),对于这8中操作,虚拟机也规定了一系列规则,在执行这8中操作的时候必须遵循如下的规则:

不允许read和load、store和write操作之一单独出现,也就是不允许从主内存读取了变量的值但是工作内存不接收的情况,或者不允许从工作内存将变量的值回写到主内存但是主内存不接收的情况

不允许一个线程丢弃最近的assign操作,也就是不允许线程在自己的工作线程中修改了变量的值却不同步/回写到主内存

不允许一个线程回写没有修改的变量到主内存,也就是如果线程工作内存中变量没有发生过任何assign操作,是不允许将该变量的值回写到主内存

新的变量只能在主内存中产生,不允许在工作内存中直接使用一个未被初始化的变量,也就是没有执行load或者assign操作。也就是说在执行use、store之前必须对相同的变量执行了load、assign操作

一个变量在同一时刻只能被一个线程对其进行lock操作,也就是说一个线程一旦对一个变量加锁后,在该线程没有释放掉锁之前,其他线程是不能对其加锁的,但是同一个线程对一个变量加锁后,可以继续加锁,同时在释放锁的时候释放锁次数必须和加锁次数相同。

对变量执行lock操作,就会清空工作内存该变量的值,执行引擎使用这个变量之前,需要重新load或者assign操作初始化变量的值

不允许对没有lock的变量执行unlock操作,如果一个变量没有被lock操作,那也不能对其执行unlock操作,当然一个线程也不能对被其他线程lock的变量执行unlock操作

对一个变量执行unlock之前,必须先把变量同步回主内存中,也就是执行store和write操作

2、volatile特性

volatile基本上是虚拟机提供的最轻量级的同步机制,用volatile修饰的变量,对所有线程可见,即对volatile变量所做的写操作能立即反映到其他线程中

2.1、volatile关键字的作用

java内存模型对volatile专门定义了一些特殊的访问规则:

假定T表示一个线程,V和W分别表示两个volatile修饰的变量,那么在进行read、load、use、assign、store和write操作的时候需要满足如下规则:

1、只有当线程T对变量V执行的前一个动作是load,线程T对变量V才能执行use动作;同时只有当线程T对变量V执行的后一个动作是use的时候线程T对变量V才能执行load操作。所以,线程T对变量V的use动作和线程T对变量V的read、load动作相关联,必须是连续一起出现。也就是在线程T的工作内存中,每次使用变量V之前必须从主内存去重新获取最新的值,用于保证线程T能看得见其他线程对变量V的最新的修改后的值。

2、只有当线程T对变量V执行的前一个动作是assign的时候,线程T对变量V才能执行store动作;同时只有当线程T对变量V执行的后一个动作是store的时候,线程T对变量V才能执行assign动作。所以,线程T对变量V的assign操作和线程T对变量V的store、write动作相关联,必须一起连续出现。也即是在线程T的工作内存中,每次修改变量V之后必须立刻同步回主内存,用于保证线程T对变量V的修改能立刻被其他线程看到。

3、假定动作A是线程T对变量V实施的use或assign动作,动作F是和动作A相关联的load或store动作,动作P是和动作F相对应的对变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或assign动作,动作G是和动作B相关联的load或store动作,动作Q是和动作G相对应的对变量W的read或write动作。如果动作A先于B,那么P先于Q。也就是说在同一个线程内部,被volatile修饰的变量不会被指令重排序,保证代码的执行顺序和程序的顺序相同。

前两条规则说明volatile类型的变量保证对所有线程的可见性。第三条规则是volatile类型的变量禁止指令重排序优化

2.2、volatile类型变量的可见性和线程安全的关系

可见性是指当一个线程修改了这个变量的值,新值(修改后的值)对于其他线程来说是立即可以得知的。正如上面前两条规则,volatile类型的变量每次被修改了就立即同步到主内存,每次使用时就从主内存重新读取值。

volatile变量对所有线程都是立即可见,对volatile变量的所有修改都立即反应到其他线程中。所以基于volatile变量的运算在并发下是线程安全的呢?

这个结论是错误的。volatile的规则,保证了read、load、use的顺序和连续性,同时也保证assign、store、write也是顺序和连续的。也就是这几个动作保证了是原子性的,但是对于变量的运算却不能保证是原子性的。如果对变量的修改是分多个步骤的,那么多个线程同时从主内存拿到的值是最新的,但是经过多步运算后回写主内存的值是有可能发生覆盖的。

下面举个例子:

public class VolatileDemo {
  public static volatile int num = 0;
  public static void increase() {
    num++;
  }

  private static final int THREADS_COUNT = 20;

  public void static main(String[] args) {
      Thread[] threads = new Thread[THREADS_COUNT);
      for (int = 0; i < THREADS_COUNT; i++) {
          threads[i] = new Thread(new Runnable(){
              @Override
              public void run() {
                  for (int j = 0; j < 10000; j++) {
                     increase();
                  }
              }
          });
          threads[i].start();
      }
      while (Thread.activeCount() > 1) {
         Thread.yield();
      }
      System.out.println(race);
  }
}

代码就是对volatile类型的变量启动了20个线程,每个线程对变量执行1w次加1操作,如果volatile变量并发操作没有问题的话,那么结果应该是输出20w,但是结果运行的时候每次都是小于20w,这就是因为num++操作不是原子性的,是分多个步骤完成的。

假设两个线程a、b同时取到了主内存的值,是0,这是没有问题的,但是可能在进行++操作的时候假设线程a执行到一半,线程b执行完了,这时线程b立即同步给了主内存,主内存的值为1,而线程a此时也执行完了,同步给了主内存,此时的值仍然是1,线程b的结果被覆盖掉了。

因此,volatile只保证了可见性,不保证原子性,也就不能保证线程安全。

2.3、指令重排

指令重排:指的是JVM为了优化,在条件允许的情况下,对指令进行一定的重新排列,直接运行当前能够立即执行的后续指令,避开获取下一条指令所需数据造成的等待

但是,指令重排机制只考虑线程内串行语义,不考虑多线程间的语义

在单线程中,虚拟机在进行指令重排过程中,会保证在需要赋值的时候结果是正确的,即使代码执行的顺序和代码顺序不一致。这种情况下,我们看起来是代码执行顺序和代码顺序是一致的,但实际上却不是。

但是在多线程中,就可能会出现异常了,下面举个案例:

Map configOptions;
char[] configText;
//volatile类型变量
volatile boolean initialized = false;

//假设以下代码在线程A中执行
//模拟读取配置信息,读取完成后认为是初始化完成
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;

//假设以下代码在线程B中执行
//等待initialized为true后,读取配置信息进行操作
while (!initialized) {
  sleep();
}
//操作读取后的配置
doSomethingWithConfig();

实际运行中,如果initialized是普通变量,没有用volatile修饰的话。在线程A中,initialized=true是可能比读取配置文件的操作早执行的。因为对线程A中进行这样的指令重排,并不会影响线程A的执行结果,JVM可以这样进行重排。如果initalized先赋值为true了,那线程B就有可能在线程A读取配置前,先执行doSomethingWithConfig()方法,程序就会发生异常。

这种情况下,用volatile修饰变量,禁止指令重排是十分有必要的。

3、小结

1、介绍java内存模型,理解主内存、工作内存,以及内存之间交互的规则

2、介绍volatile原理,对应到内存模型操作规则的变化

3、介绍volatile可见性及禁止指令重排的使用