java内存模型JMM

简介: Java内存模型(Java Memory Model,简称JMM),即Java虚拟机定义的一种用来屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能够达到一致的内存访问效果的内存模型。本篇文章大致涉及到五个要点:1、Java内存模型的基础,主要介绍JMM抽象结构;2、Java内存模型中内存屏障;3、Java内存模型中的重排序;4、happens-before原则;JMM相关的三个同步原语(synchronized,volatile,final)。

Java内存模型(Java Memory Model,简称JMM),即Java虚拟机定义的一种用来屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能够达到一致的内存访问效果的内存模型。本篇文章大致涉及到五个要点:1、Java内存模型的基础,主要介绍JMM抽象结构;2、Java内存模型中内存屏障;3、Java内存模型中的重排序;4、happens-before原则;JMM相关的三个同步原语(synchronized,volatile,final)。

1.Java内存模型的抽象结构

在java中,共享变量是指所有存储在堆内存中的实例字段,静态字段和数组对象元素,因为堆内存是所有线程共享的数据区。而局部变量,方法定义参数,异常处理参数不会在线程之间共享,它们不存在内存可见性问题,也不会受到Java内存模型的影响。

Java内存模型决定了一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,Java内存模型定义了线程与主内存之间的抽象关系:线程之间的共享变量存储主内存中,每个线程都有一个私有的本地内存,也叫工作内存,本地内存存储了该线程需要读/写的共享变量的副本。本地内存是JMM的一个抽象的概念,其实并不真实存在。Java内存模型的抽象示意图如下:
image.png

从上图来看,如果线程A和线程B之间要通信的话,必须要经历下面的两个过程:

1.线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2.线程B到主内存中去读取线程A之前已更新过的共享变量。

下面通过示意图说明以上两个过程:
image.png

如上图:假设初始时,X的值为0,首先线程A要先从主内存中读取共享变量x的值,并将其副本存储在自己的本地内存。接着线程A要把共享变量x的值更新为1,也就是先把本地内存中的x的副本的值更新为1,然后再把本地内存中刚更新过的共享变量刷新到主内存,此时主内存中共享变量x的值为1。然后线程A向线程B发送通知:哥们儿,我已更新了共享变量的值。

随后,线程B接收到线程A发送的通知,也从主内存中读取共享变量x的值,并将其副本存储在自己的本地内存,接着线程B也要修改共享变量的值,先将本地内存B中的副本x修改为2,再将本地内存中的x的值刷新到主内存,此时主内存中共享变量x的值就被更新为了2。

从整体上来看,上述的两个过程实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

2.Java内存模型的内存屏障

为了保证内存的可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令类禁止特定类型的处理器重排序,java内存模型(JMM)把内存屏障指令分为4类:

  • LoadLoad(Load1,LoadLoad,Load2):确保load1数据的装载先于load2及所有后序装载指令的装载。
  • LoadStore(Load1,LoadStore,Store2):确保Load1数据的装载先于Store2及所有后序存储指令刷新内存。
  • StoreStore(Store1,StoreStore,Store2):确保Store1数据刷新内存先于Store2及所有后序存储指令刷新内存。
  • StoreLoad(Store1,StoreLoad,Load2):确保Store1数据刷新内存先于Load2及所有后序装载指令的装载。该屏蔽指令会使该屏蔽之前的所有内存访问指令执行完成后才执行屏蔽之后的内存访问指令。并且这个指令是一个全能的指令,同时具备以上三个内存屏蔽指令的功能。

3.Java内存模型中的重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重排序的一种手段。重排序分为3种类型:

  1. 编译器优化重排序。编译器在不改变单线程语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓冲和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从java源代码到最终实际执行的指令序列,会经历下面3种重排序:
image.png

上述1属于编译器重排序,编译器将java源码编译成字节码时进行一次重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成字节码指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。

JMM属于语言级别的内存模型,它确保在不同的编译器和不同的处理器平台上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的的内存可见性保证。

3.1 数据依赖性

如果两个操作访问同一个共享变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖性分为以下3种类型:

3.3 重排序对多线程的影响

重排序会可能影响多线程程序的执行结果,请看下面的示例代码:

public class ReorderExample{
    int a = 0; 
    boolean flag = false; 
    
    @Test
    public void writer(){
        a = 1;              //1
        flag = true;     //2
    }
    
    @Test
    public void reader(){
        if(flag){           //3
            int i = a;     //4
            System.out.print(i);
        }
    }
}

flag变量是个标记,用来标识变量a是否被写入。这里我们假设有两个线程A和B,线程A首先执行writer方法,随后线程B执行reader方法。问题是线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入呢?

答案是否定的,并不一定能看到。

由于操作1和操作2不存在数据依赖关系,编译器和处理器可以对这两个操作重排序;同理,操作3和操作4也不存在数据依赖关系,编译器和处理器也可以对这两个操作重排序。下面我们先来看下,当操作1和操作2重排序时,会产生什么效果。程序执行时序图如下:

image.png

如上图,操作1和操作2做了重排序。程序执行时,线程A首先将标记变量flag写为true,随后线程B读取这个变量,由于条件为真,线程B将读取共享变量a,而此时,共享变量a还没有被线程A写入,所以多线程程序的语义就被重排序破坏了。

下面再看下,当操作3和操作4重排序时会产生什么效果。下面是操作3和操作4重排序后程序的执行时序图:

image.png

在程序中,操作3和操作4存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度,为此,编译器和处理器会采用猜测执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前读取共享变量a,然后会把共享变量a的值保存到一个名为重排序缓冲(Reorder Buffer,ROD)的硬件缓存中。当操作3的条件为真时,就把保存到ROB中的共享变量a的值写入到变量i中。

从上图中我们也可以看出,猜测执行实质上对操作3和操作4做了重排序。重排序破坏了多线程程序的语义。

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程中程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

  1. happens-before原则

从JDK1.5开始,Java使用新的JSR-133内存模型,该模型使用happens-before原则来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这里提到的两个操作既可以是在单线程内,也可以在多线程之间。

happens-before规则如下:

  • 程序顺序规则(Program Order Rule): 一个线程中的每个操作先行发生于该线程中的后序任意操作。
  • 监视器锁规则(Monitor Lock Rule): 对一个锁的解锁先行发生于随后对这个锁的加锁。
  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写,先行发生于任意后序对这个volatile变量的读。
  • 传递性(Transitivity):如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
  • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程终止规格(Thread Termination Rule):线程中所有的操作都优先发生于此线程的终止操作。‘’
  • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
  • 对象终结规则(Finalizer Rule):一个对象初始化完成先行发生于它的finalize()方法的开始。
目录
相关文章
|
16天前
|
监控 算法 Java
Java中的内存管理:理解Garbage Collection机制
本文将深入探讨Java编程语言中的内存管理,特别是垃圾回收(Garbage Collection, GC)机制。我们将从基础概念开始,逐步解析垃圾回收的工作原理、不同类型的垃圾回收器以及它们在实际项目中的应用。通过实际案例,读者将能更好地理解Java应用的性能调优技巧及最佳实践。
58 0
|
10天前
|
存储 缓存 Java
java线程内存模型底层实现原理
java线程内存模型底层实现原理
java线程内存模型底层实现原理
|
6天前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
18 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
12天前
|
Java 编译器
深入理解Java内存模型:从基础到高级
本文旨在通过通俗易懂的方式,引导读者深入理解Java内存模型(JMM)的核心概念和工作原理。我们将从简单的基础知识入手,逐步探讨重排序、顺序一致性问题以及volatile关键字的实现机制等高级主题。希望通过这篇文章,你能够对Java内存模型有一个清晰、全面的认识,并在实际编程中有效地避免并发问题。
|
10天前
|
存储 算法 Java
深入理解Java内存管理
本文将通过通俗易懂的语言,详细解析Java的内存管理机制。从JVM的内存结构入手,探讨堆、栈、方法区等区域的具体作用和原理。进一步分析垃圾回收机制及其调优方法,最后讨论内存泄漏的常见场景及防范措施。希望通过这篇文章,帮助读者更好地理解和优化Java应用的内存使用。
|
14天前
|
监控 算法 Java
Java中的内存管理与垃圾回收机制
本文将深入探讨Java编程语言中的内存管理方式,特别是垃圾回收(Garbage Collection, GC)机制。我们将了解Java虚拟机(JVM)如何自动管理内存,包括对象创建、内存分配以及不使用对象的回收过程。同时,我们还将讨论不同的垃圾回收算法及其在不同场景下的应用。
|
13天前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
20天前
|
监控 算法 Java
Java中的内存管理:理解垃圾回收机制的深度剖析
在Java编程语言中,内存管理是一个核心概念。本文将深入探讨Java的垃圾回收(GC)机制,解析其工作原理、重要性以及优化方法。通过本文,您不仅会了解到基础的GC知识,还将掌握如何在实际开发中高效利用这一机制。
|
2月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
|
3月前
|
存储 分布式计算 Hadoop
HadoopCPU、内存、存储限制
【7月更文挑战第13天】
215 14

热门文章

最新文章

下一篇
无影云桌面