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

2018-06-01  本文已影响66人  蓝色_fea0

Java内存模型

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

1/1 主内存与工作内存

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


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

1/2 内存间交互操作

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

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

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

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; i<T; i++){
    threads[i] = new Thread(
      @Override
      public void run(){
        for(int i=0; i<10000; i++){
          increase();
        }
      }
    ); 
  threads[i].start();
  }
//等待所有累加线程都结束
while(Thread.activeCount()>1){
  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包中的锁)低。

上一篇 下一篇

猜你喜欢

热点阅读