JVM 基础 - JVM 内存模型



文章目录

  • JVM 基础 - JVM 内存模型
  • 前言
  • 一、Java 内存模型(Java Memory Model)
  • 1、Java堆栈
  • 2、JMM概述
  • 3、重排序问题
  • 4、volatile关键字
  • 防重排序
  • 实现可见性
  • 保证单次的读/写操作具有原子性
  • 问题: i++为什么不能保证原子性?
  • 5、先行发生规则(happens-before)



前言

很多人总是将Java内存模型和Java内存结构搞混,本文将详细讲解在Java中,什么是Java内存模型(Java Memory Model)。如果你需要了解Java内存结构的相关知识,请看另一篇文章: 《JVM 基础 - JVM 内存结构详解》。


一、Java 内存模型(Java Memory Model)

1、Java堆栈

JVM内部使用的Java内存模型在线程栈和堆之间划分内存。

java JVM内存模型面试 jvm 的内存模型_Java

2、JMM概述

Java 内存模型的主要目标是定义程序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。

Java 内存模型中规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的主内存中的变量拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递(通信)均需要在主内存中来完成。

Java 内存模型的抽象示意图如下:

java JVM内存模型面试 jvm 的内存模型_Java_02

3、重排序问题

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

1、编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

2、指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3、内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

java JVM内存模型面试 jvm 的内存模型_内存模型_03


比如:A a = new A() 实际上是通过三个步骤完成,1:开辟一片内存空间,2:在内存空间初始化对象数据,3:将内存空间的地址赋值给对应的引用。由于重排序,可能实际执行的时候会变成:1:开辟一片内存空间,2:将内存空间的地址赋值给对应的引用,3:在内存空间初始化对象数据。如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。

4、volatile关键字

防重排序

volatile的一个主要作用是防止重排序,也就是说,使用volatile关键字修饰的变量,在实际运行过程中,不会出现上述所说的重排,保证了程序的顺序执行。

实现可见性

volatile的另一个作用是实现变量的可见性,通俗的讲,就是一个线程修改了变量的值,另外的线程能够立马知道。

保证单次的读/写操作具有原子性

volatile不能保证完全的原子性,只能保证单次的读/写操作具有原子性。

问题: i++为什么不能保证原子性?

因为i++本质上是读、写两次操作,它是一个复合操作,包括三步骤:
1、读取i的值
2、对i加1
3、将i的值写回内存

运行如下代码:

public class VolatileTest {
    volatile int i;

    public void add(){
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        final  VolatileTest test = new VolatileTest();
        for (int n = 0; n < 1000; n++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.add();
                }
            }).start();
        }
        Thread.sleep(10000);//等待10秒,保证上面程序执行完成
        System.out.println(test.i);
    }
}

期望结果:i=1000,实际上i的值可能小于1000,证明volatile是无法保证 i++ 这三个操作的原子性的,可以通过AtomicInteger或者Synchronized来保证+1操作的原子性。

5、先行发生规则(happens-before)

JMM 为了保证有序,还内置了一套先行发生规则(happens-before)。两个操作间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行。happens-before 仅仅要求前一个操作对后一个操作可见,和一般意义上时间的先后是不一样的,达到逻辑上的顺序执行即可。

具体的规则如下:

1 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生与书写在后面的操作。【保证单线程的有序】

2 锁定规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。

3 volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。【先写后读】

4 传递规则:A 先于 B 且 B 先于 C 则 A 先于 C

5 线程启动规则:Thread 对象的 start 方法先行发生于此线程的每一个动作。

6 线程中断规则:对线程 interrupt 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。【先中断,后检测】

7 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join() 方法结束,Thread.isAlive() 的返回值手段检测线程已经终止执行。

8 对象终结规则:一个对象的初始化完成先行发生于它的 finalize 方法的开始。

如果两个操作的执行顺序不能通过 happens-before 原则推导出来,就不能保证他们的执行次序,虚拟机就可以随意的对他们进行重排序。