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()方法的开始。
目录
相关文章
|
22天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
24 0
|
24天前
|
存储 监控 算法
Java内存管理深度剖析:从垃圾收集到内存泄漏的全面指南####
本文深入探讨了Java虚拟机(JVM)中的内存管理机制,特别是垃圾收集(GC)的工作原理及其调优策略。不同于传统的摘要概述,本文将通过实际案例分析,揭示内存泄漏的根源与预防措施,为开发者提供实战中的优化建议,旨在帮助读者构建高效、稳定的Java应用。 ####
37 8
|
22天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
26天前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
53 5
|
24天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
24天前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
29天前
|
算法 Java 开发者
Java内存管理与垃圾回收机制深度剖析####
本文深入探讨了Java虚拟机(JVM)的内存管理机制,特别是其垃圾回收机制的工作原理、算法及实践优化策略。不同于传统的摘要概述,本文将以一个虚拟的“城市环卫系统”为比喻,生动形象地揭示Java内存管理的奥秘,旨在帮助开发者更好地理解并调优Java应用的性能。 ####
|
21天前
|
存储 监控 算法
Java内存管理的艺术:深入理解垃圾回收机制####
本文将引领读者探索Java虚拟机(JVM)中垃圾回收的奥秘,解析其背后的算法原理,通过实例揭示调优策略,旨在提升Java开发者对内存管理能力的认知,优化应用程序性能。 ####
36 0
|
存储 缓存 安全
基于JVM原理、JMM模型和CPU缓存模型深入理解Java并发编程
许多以Java多线程开发为主题的技术书籍,都会把对Java虚拟机和Java内存模型的讲解,作为讲授Java并发编程开发的主要内容,有的还深入到计算机系统的内存、CPU、缓存等予以说明。实际上,在实际的Java开发工作中,仅仅了解并发编程的创建、启动、管理和通信等基本知识还是不够的。
3972 0
|
2天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者

热门文章

最新文章