JVM相关知识(内存模型篇)

82 阅读2分钟

JMM

在 java 中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享。 JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读 / 写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不客观存在。

线程栈到底放了什么?

  • 线程栈包含正在执行的每个方法的所有局部变量(调用堆栈上的所有方法), 基本类型的所有局部变量(boolean,byte,short,char,int,long,float,double)完全存储在线程堆栈。
  • 堆包含了在Java应用程序中创建的所有对象,无论创建该对象的线程是什么。 这包括基本类型的包装类(例如Byte,Integer,Long等)。 无论是创建对象并将其分配给局部变量,还是创建为另一个对象的成员变量,该对象仍然存储在堆上。
  • 局部变量可以是基本类型,在这种情况下,它完全保留在线程堆栈上。
  • 局部变量也可以是对象的引用。 在这种情况下,引用(局部变量)存储在线程堆栈中,但是对象本身存储在堆(Heap)上。
  • 对象的成员变量与对象本身一起存储在堆上。 当成员变量是基本类型时,以及它是对象的引用时都是如此。
  • 静态类变量也与类定义一起存储在堆上。

并发问题三要素

可见性

可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。

原子性

一组操作要么全部执行,要么都不执行,中途不会被中断。

有序性

程序执行的顺序按照代码书写的顺序执行。

Volitile只保障了可见性和有序性,并不能保障原子性。举一个很出名的例子说明下:

public class SingleInstance {
  private volatile static SingleInstance instance;
  public static SingleInstance getInstance() {   //1
    if (instance == null) {   //2
      synchronized(SingleInstance.class) {   //3
        if(instance == null) {   //4
          instance = new SingleInstance(); //5
        }
      }
    }
    return instance;   //6
  }
}

上例中的第5个步骤的代码, new SingleInstance()并不是原子性的, 在jvm中被拆分为三个步骤:

a. memory = allocate() //分配内存 
b. ctorInstanc(memory) //初始化对象 
c. instance = memory   //设置instance指向刚分配的地址

如果不加volatile字段(保障有序和可见性), 在多线程的并发下,可能出现这样一种情况, 比如A线程执行到第5步, 然后由于jvm指令重排序,abc三个步骤中执行了a和c, 此时B线程执行到了第2步,判断instance != null(A线程执行了c),导致B线程返回未初始化的实例,使得程序异常。