文章目录

  • 1.Java内存模型
  • 1.1 主内存 和 工作内存
  • 1.2 内存间交互操作
  • Java内存模型的三大特性
  • happens-before 先行发生原则
  • 2.volatile型变量的特殊规则
  • 2.1 变量对所有线程可见
  • 2.2 禁止指令重排序
  • 2.3 单例模式中的Double check


1.Java内存模型

JVM定义了一种Java内存模型来 屏蔽掉各种硬件和操作系统的之间的交互,不像c++中的内存模型,不同的数据类型在不同的平台上长度不一样,Java中不管哪个基本数据类型,在不同的平台上长度都是一模一样的。使得Java程序在各种平台下都能达到一致的内存访问效果。

1.1 主内存 和 工作内存

JVM内存模型的主要目标是定义程序中各个变量的访问规则,即JVM将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量包含实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为后两者是私有的,不会被线程共享。

JVM内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了 被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存中的变量,不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者的交互关系如下:

java内存模型指令 java内存模型原理_Java内幕才能模型的三大特性

1.2 内存间交互操作

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存中拷贝到工作内存、如何从工作内存同步回到内存之类的实现细节。Java内存模型中定义了以下8种操作来完成。JVM实现时必须保证下面提及的每一种操作是原子的、不可再分的

  • lock(锁定):作用于主内存中的变量,它把一个变量标识为一条线程独占的状态
  • unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存中的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎
  • assign(赋值):作用于工作内存中的变量,它把一个执行引擎接收到的值赋给工作内存中的变量
  • store(存储):作用于工作内存中的变量,它把工作内存中一个变量的值送到主内存中,以便后续的write操作使用
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

Java内存模型的三大特性

  1. 原子性:由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、write。大致可以认为基本数据类型的访问读写是具备原子性的,如若需要更大范围的原子性,需要synchronized关键字约束。(即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行)
  2. 可见性:指当一个线程修改了共享变量的值,其他线程能够立即知道这个修改。volatile、synchronized、final三个关键字可以实现原子性
  3. 有序性:如果在本线程内观察,所有的操作都是有序的,如果在线程中观察另外一个线程,所有的操作都是无序的,前半句是指"线程内表现为串行",后半句是指"指令重排序"和"工作内存与主内存同步延迟"现象

Java内存模型具备一些先天的"有序性",即不需要通过任何的手段就能够得到保证的有序性,这个通常也称为happens-before原则,如果两个操作的执行次序无法从happens-before原则推导出来,那么就不能保证它们的有序性,虚拟机可以随意的对它们进行重排序。

happens-before 先行发生原则

  • 程序次序原则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:unlock解锁操作先行发生于后面对同一个锁的lock加锁操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个线程的读操作
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都在线程终止前发生
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

也就是说,要想并发程序的正确执行,必须要保证原子性、可见性以及有序性,只要有一个没有被保证,就会导致程序运行不正确。

2.volatile型变量的特殊规则

关键字volatile可以说是JVM提供的最轻量级的同步机制,JVM内存模型读volatile专门定义了一些特殊的访问规则。
当一个变量定义为volatile之后,它将具备两种特性:

2.1 变量对所有线程可见

保证此变量对所有线程的可见性,这里的"可见性"是指:当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的,而普通变量做不到这一点,普通变量的值在线程间传递均需要荣光主内存来完成。例如:线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成之后再从主内存进行读取操作,新值才会对线程B可见。

volatile变量在各个线程中是一致的,但是volatile变量的运算在并发下一样是不安全的

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

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

java内存模型指令 java内存模型原理_单例模式Double check_02


问题在于num++之中,实际上num++等同于num = num+1,volatile关键字保证了num的值在取值时是正确的,但是在执行num+1的时候,其他线程可能已经把num的值增大了,这样+1之后会把较小的数值同步回主存之中。

由于volatile关键字只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然需要通过加锁(synchronized或者lock)来保证原子性

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
  • 变量不需要与其它的状态变量共同参与不变约束

2.2 禁止指令重排序

使用volatile变量的语义是禁止指令重排序,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序和程序代码中执行的顺序一样
volatile关键字禁止指令重排序有两层意思

  • 当程序执行volatile变量的读操作或者写操作时,在其前面的操作肯定全部已经进行完毕,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行
  • 在进行指令优化时,不能将volatile变量前面的语句放到其后面执行,也不能把volatile变量后面的语句放到其前面执行。

// x,y为非valatile变量
//flag为volatile变量
x = 2; 语句1
y = 0; 语句2
flag = true;
x = 4; 语句3
y = -1; 语句4

由于flag变量为volatile变量,那么在进行指令重排序的过程中,不会把语句2 放在语句1,语句2 前面,也不会将语句3 放到语句4,语句5后面。
并且volatile关键字能保证,执行到语句3 ,语句1和语句2一定执行完毕,且结果对语句3,语句4,语句5是可见的
指令重排序

Map configOptions;
char[] configText;
volatile boolean initialized = false;
//假设以下代码在线程A执行
//模拟读取配置文件信息,当读取完成后将initialized设置为true以通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized = true;
//假设以下代码在线程B执行
//等待initialized为true,代表线程A已经把配置信息初始化完成
while(!initialized) {
  sleep();
}
//使用线程A初始化好的配置信息
doSomethingWithConfig();

initialized设置为volatile保证在initialized变为true之后,所有的配置文件信息已经读取成功

2.3 单例模式中的Double check

双重检验锁模式,是一种使用同步块加锁的方法,会有两次检查instance == null,一次是在同步块外,一次是在同步块内。为什么同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的if,如果同步块内不进行二次检验的话就会产生多个实例

public static Singleton getSingleton(){
    if(instance==null){ //Single Checked
	synchronized (Singleton.class){
   		if(instance==null){ //Double Checked
   			instance=new Singleton();
  		 }
   }
 }
    return instance;
}

这段代码实际上也是有很大的问题,主要在于instance=new Singleton()这句,这并非是一个原子操作,在JVM中这句话做了三件事1、给instance分配内存  2、调用Singleton的构造函数来初始化成员变量 3、将instance对象指向分配的内存空间,执行完这三步instance就为非null的了。但是JVM存在指令重排序的优化,也就是说上面的步骤是不能保证的,最终执行的顺序可能是1-2-3也可能是1-3-2.如果是后者,在3执行完毕2执行之前被线程二抢占了,这时instance已经是非null的了(但却没有初始化),线程2会直接返回instance,后面自然而言就会出错。
为避免上述错误,我们只需要将instance变量声明成volatile就可以了。

class Singleton{
 //禁止指令重排序
  private volatile static Singleton instance = null;
  private Singleton() {
   
 }
  public static Singleton getInstance() {
    if(instance==null) {
      synchronized (Singleton.class) {
        if(instance==null)
          instance = new Singleton();
     }
   }
    return instance;
 }
}