《深入理解Java虚拟机》(三)--Java内存模型与线程(1)

Java内存模型

Java的内存模型屏蔽掉了各种硬件和操作系统的内存访问差异,实现了Java跨平台的效果,C/C++语言使用的是物理硬件和操作系统的内存模型,所以不能实现跨平台。

1/1 主内存与工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,这里说的变量包括实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为他们是线程私有的。
Java内存模型规定了所有的变量都存储在主内存上,每条线程有自己的工作内存,工作内存中保存了被该线程使用到的变量的内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写在主内存中的变量。不同的线程也无法访问对方的工作内存,线程之间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者之间的交互关系如下:


线程、主内存、工作内存三者之间的交互关系

1/2 内存间交互操作

Java内存模型定义了以下8中操作来完成主内存与工作内存之间的数据交互。

  • lock(锁定):作用于主内存变量,将一个变量标记为一条线程独占的状态。
  • unlock(解锁):作用于主内存变量,它把一个处于锁定状态的变量释放出来,释放后变量才能被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作的使用。
    -load(载入):作用于工作内存中的变量,它把read操作的从主内存中得到的变量放入到工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎中得到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令的时候执行这个操作。
  • store(储存):作用于工作内存中,它把工作内存中的一个变量的值传送到主内存中,以便后面write的使用。
  • write(写入):作用于主内存中,它把stroe从工作内存中得到的变量的值放到指定的主内存的变量中。
    如果要把一个变量从主内存复制到工作内存,就要使用read load操作,如果要从工作内存同步回主内存就要使用store write操作,Java内存模型只要求这几个操作顺序执行,并没有要求连续执行(也就是他们之中可以插入别的操作),除此之外,Java内存模型还规定了上述8种操作必须满足如下规则:
  • 不允许read和load、store和write单独出现。
  • 不允许一个线程丢弃它最近的的assign操作,也就是说一个线程改变了它的变量之后必须同步到主内存中去。
  • 不允许一个线程没有发生过任何assign操作就把数据从工作内存同步到主内存中去。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量。
  • 一个变量在同一时刻只允许一条线程对其lock,执行了多少次lock就要对应执行多少次unlock。
  • 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量之前,需要重新执行load或者assign操作。
  • 如果一个线程没有执行lock,就不许执行unlock,也不允许unlock一个被其他线程锁定住的变量。
  • 对一个变量执行unlock之前必须先把它同步到主内存之中。

1/3 volatile 型变量的特殊规则

被volatile修饰的变量具备两个特性,一个是保证此变量对所有线程的可见性,另一个是禁止指令重排序。

  • 可见性:
    对所有线程的可见性是指当一个线程修改了这个变量的值,这个值对于其他线程来说是立即可以见的。但这只是赋值操作,如果这个操作是一个运算(例如 a++;)。
    举个栗子:
public class Vola {
  public static volatitle int race = 0;
  public static void increase(){
     race++; 
  }
  private static final int T = 20;
  public static void main(String[] args){
  Thread[] threads = new Thread[T];
  for(int i=0; i1){
  Thread.yield();
}
System.out.println(race);
  }
}

这段代码发起了20个线程,每个线程对race变量进行1W次自增操作,如果能够正确并发的话,最后输出的结果应该是20W,但是结果却是一个小于20W的数。问题就是在于“race++”中,虽然在Java代码中它是一条命令,但是在Class文件中它是由四条字节码指令构成的

0:  getstatic #13;
3:  iconst_1
4:  iadd
5:  putstatic #13

当getstatic把race取出来时,volatile确实保证了race的值在此时的正确性,但是在执行iconst_1、iadd这些指令的时候,其他线程已经把race的值改变了。我的理解就是因为race++这句java代码不是原子性的。而race = 1或者race=null则可以保证原子性。

  • 禁止指令重排序
    什么是指令重排序?举个栗子
int a = 1;
int b = 2;

这两条java语句没有任何关联,java虚拟机在执行的过程中,为了优化代码执行的效率,可能会将两条语句(或者多条语句)在不改变其结果的情况下重新排序,然后执行。不光java虚拟机有指令重排序,CPU处理器也可能对输入的代码进行乱序执行优化。
举个栗子来看看指令重排序的影响:

Map config;
char[] configText;

//次变量必须为volatile变量
volatile boolean init = false;

//假设以下代码在线程A中执行
//模拟读取配置信息,当读取完成后将init的值设置为true来通知其他线程配置可用
config = new HashMap();
//读取配置文件
configText = readConfigFile(fileName);
//加载配置文件
proConfig(configText,config);
init = true;



//设置以下代码在线程B中执行
//等待init为true,代表线程A已经初始化配置文件完毕
while(!init){
    sleep();
}
//使用线程A中初始化的配置信息
dosomething(....);

如果init没有被volatile修饰,就可能由于指令重排序的优化,导致init = true;在还没有加载完成配置文件的时候被提前执行,这样B中使用配置信息的代码就可能出错自。volatile关键字则可以避免这种情况发生。
使用volatile修饰的变量会有一个内存屏障,指令重排序的时候不会把内存屏障后面的指令排到指令屏障的前面。
在大多数场景下,volatile的总开销要比锁(synchronize或者java.util.concurrent包中的锁)低。

你可能感兴趣的:(《深入理解Java虚拟机》(三)--Java内存模型与线程(1))