J.U.C Review - 白话Java内存模型

2024-09-02 01:36
文章标签 java 内存 模型 review 白话

本文主要是介绍J.U.C Review - 白话Java内存模型,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 并发编程要解决的问题
  • 运行时内存的划分
  • 内存可见性问题及其解决方法
    • JMM的抽象示意图
  • Java内存模型与JVM内存区域划分的关系
  • 重排序与happens-before
    • 什么是重排序?
    • 重排序的类型
    • 顺序一致性模型与JMM的保证
      • 顺序一致性模型
      • Java内存模型(JMM)的保证
  • happens-before原则
    • 什么是happens-before
    • 天然的happens-before关系
    • 实例分析
    • 小结

在这里插入图片描述


并发编程要解决的问题

  • 线程间如何通信? —> 线程之间以何种机制来交换信息

  • 线程间如何同步? —> 线程以何种机制来控制不同线程间操作发生的相对顺序


来听个故事: 在现代计算机世界里,想象有一个忙碌的小镇。这个小镇上有许多居民,每个居民都代表着一个线程。为了让这个小镇繁荣发展,每个居民(线程)都需要与其他居民协作(通信),并且每个人都需要遵守一定的秩序(同步),避免发生混乱。

在这个小镇上,有两个主要的方式让居民之间进行交流:

  1. 消息传递并发模型:居民们通过信使互相发送信息。每次一个居民想要告诉另一个居民什么事时,他会写一封信交给信使,而信使会把这封信送到目的地。这种方式很简单,信息从一个居民直接传递到另一个居民,没有中间环节。

  2. 共享内存并发模型:居民们通过一个公告板来共享信息。每个居民都有自己的一块小黑板,他们会定期去公告板上查看信息,或者将信息更新在公告板上。每个人都可以看到公告板上发布的信息,尽管他们可能会在不同时间去查看。

Java并发模型采用的是第二种——共享内存并发模型。 在这个模型里,所有线程都共享一块“公告板”,即主内存


运行时内存的划分

让我们继续进一步了解Java小镇的内存系统。

在小镇的每个居民家中,有一个小黑板,这个小黑板相当于每个线程的本地内存。居民们在处理事务时,首先会在自己的小黑板上记录(即本地内存),而不是直接去公告板上操作。这样做的好处是效率更高,因为直接在自己家里的小黑板上写东西比跑到公告板上去要快得多。

那么,小镇的内存是如何划分的呢?Java小镇的内存分为两大块:(Stack)和(Heap)。

  1. :每个居民都有一个自己的小空间(栈),用来存放一些个人的东西,比如短期内需要的物品(局部变量、方法参数等)。这个地方是完全私有的,其他居民是看不到这些物品的。

  2. :所有居民共同使用的一个大仓库(堆),用来存放一些共享的物品。这些物品是大家都能看到的(共享变量),但也正因为如此,可能会产生一些麻烦,比如物品的位置可能被别人移动了(内存不可见性)。

在这里插入图片描述


内存可见性问题及其解决方法

问题来了,既然堆是共享的,那为什么会有内存不可见性的问题呢?

原因在于小镇的居民有时会偷懒,不是每次都跑到公告板上去,而是把一些经常用的信息记在自己家里的小黑板上(缓存)。这样虽然方便了自己,却导致了其他居民无法看到最新的公告(共享变量的值)。

比如,居民A更新了公告板上的一条信息,但他没有马上去通知其他人,这时居民B如果去查看公告板,可能看到的还是旧的信息。只有当居民A把自己小黑板上的信息同步到公告板(主内存)时,居民B才会看到更新后的信息。

这时候,Java小镇上的Java内存模型(JMM)发挥了作用。JMM定义了居民们与公告板之间的互动规则,确保信息的更新不会出现混乱。通过这些规则,JMM保证了共享信息的可见性。换句话说,当居民A在自己的小黑板上更新了信息后,JMM会确保居民B最终能够看到这个更新。

JMM的抽象示意图

在这里插入图片描述

从图中可以看出:

  • 所有的共享变量都存在主内存中。

  • 每个线程都保存了一份该线程使用到的共享变量的副本。

  • 如果线程A与线程B之间要通信的话,必须经历下面2个步骤:

    • 线程A将本地内存A中更新过的共享变量刷新到主内存中去。

    • 线程B到主内存中去读取线程A之前已经更新过的共享变量。

所以,线程A无法直接访问线程B的工作内存,线程间通信必须经过主内存。

注意,根据JMM的规定,线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读取。

所以线程B并不是直接去主内存中读取共享变量的值,而是先在本地内存B中找到这个共享变量,发现这个共享变量已经被更新了,然后本地内存B去主内存中读取这个共享变量的新值,并拷贝到本地内存B中,最后线程B再读取本地内存B中的新值

Java中的volatile关键字可以保证多线程操作共享变量的可见性以及禁止指令重排序,synchronized关键字不仅保证可见性,同时也保证了原子性(互斥性)。在更底层,JMM通过内存屏障来实现内存的可见性以及禁止重排序。为了程序员的方便理解,提出了happens-before,它更加的简单易懂,从而避免了程序员为了理解内存可见性而去学习复杂的重排序规则以及这些规则的具体实现方法


Java内存模型与JVM内存区域划分的关系

最后,回到Java小镇的内存划分问题。我们刚才提到的JMM实际上是一种抽象的规则集,它定义了居民如何使用公告板和小黑板 (围绕原子性、有序性、可见性等展开的),而Java运行时内存区域的划分则是具体的内存管理方式。

简单来说,JMM是从逻辑上规定了居民们的行为,而Java运行时内存的划分则是从物理上分配了小黑板、公告板的空间。两者虽然是不同层次的概念,但它们密切相关。

  • JMM的主内存通常对应于Java运行时内存区域中的方法区
  • JMM的本地内存则对应于程序计数器等私有数据区域。

尽管概念上有些不同,但它们表达的是同一种内存结构


重排序与happens-before

什么是重排序?

指令重排序是计算机执行程序时,为提高性能而对指令执行顺序进行调整的一种优化手段。这种优化在单线程中不会改变程序的语义,但在多线程环境中可能导致不一致的内存可见性


重排序的类型

  1. 编译器优化重排序:编译器在不改变单线程语义的前提下重新安排指令顺序,以提高执行效率。例如,可以将无依赖关系的指令调换顺序以减少执行时间。
  2. 指令并行重排序:处理器利用指令级并行技术同时执行多条指令,只要这些指令之间没有数据依赖关系,就可以改变其执行顺序。
  3. 内存系统重排序:由于处理器使用了缓存和缓存一致性协议,内存读写操作在不同线程中的可见性顺序可能会不同,导致内存系统表现出重排序现象。

顺序一致性模型与JMM的保证

顺序一致性模型

顺序一致性模型是一种理想化的内存模型,它假设所有的操作严格按照程序的顺序执行,并且所有线程看到的执行顺序是一致的。在这种模型下,所有操作的执行顺序都具有原子性可见性

Java内存模型(JMM)的保证

JMM提供了一种更为实际的模型,在确保程序正确性的前提下,允许编译器和处理器进行优化。JMM通过以下方式保证多线程程序的正确性:

  • 正确同步的程序在JMM中执行的结果与顺序一致性模型中的执行结果相同
  • 对于未同步的程序,JMM只提供最小的安全性,即线程读取到的值要么是之前某个线程写入的值,要么是默认值。

JMM在保证程序正确性的同时,也尽可能允许编译器和处理器进行优化,以提升程序的执行性能。


happens-before原则

什么是happens-before

happens-before是JMM用于描述操作之间内存可见性关系的概念。如果操作A happens-before操作B,则A的结果对B可见,并且A的执行顺序在B之前。

JMM通过happens-before规则来为多线程编程提供内存可见性的保证,确保正确同步的多线程程序执行结果不被重排序所改变。

天然的happens-before关系

在Java中,有以下几种天然的happens-before关系:

  1. 程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile变量的写操作,happens-before于任意后续对该volatile变量的读操作。
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  5. 线程启动规则:如果线程A执行操作ThreadB.start()启动线程B,那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  6. 线程终止规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

实例分析

以下代码展示了happens-before的使用:

int a = 1; // A操作
int b = 2; // B操作
int sum = a + b; // C 操作
System.out.println(sum);

在这个单线程的例子中,根据程序顺序规则,我们可以确定:

  • A happens-before B
  • B happens-before C
  • A happens-before C

这种情况下,即使JVM在实际执行时对A和B进行了重排序,由于没有影响到程序的最终结果,JMM允许这种重排序。

小结

指令重排序通过优化程序执行顺序提高了CPU性能,但也引入了潜在的多线程问题。为了确保多线程程序的正确性,JMM引入了happens-before原则,为开发者提供了一套简单易懂的规则,确保了程序的内存可见性和正确执行。

JMM通过限制对影响程序正确性的重排序,确保了程序的执行结果,同时允许那些不改变程序执行结果的重排序,以最大限度地发挥编译器和处理器的优化能力。


这篇关于J.U.C Review - 白话Java内存模型的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1128625

相关文章

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

SpringBoot全局域名替换的实现

《SpringBoot全局域名替换的实现》本文主要介绍了SpringBoot全局域名替换的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录 项目结构⚙️ 配置文件application.yml️ 配置类AppProperties.Ja

Java使用Javassist动态生成HelloWorld类

《Java使用Javassist动态生成HelloWorld类》Javassist是一个非常强大的字节码操作和定义库,它允许开发者在运行时创建新的类或者修改现有的类,本文将简单介绍如何使用Javass... 目录1. Javassist简介2. 环境准备3. 动态生成HelloWorld类3.1 创建CtC

JavaScript中的高级调试方法全攻略指南

《JavaScript中的高级调试方法全攻略指南》什么是高级JavaScript调试技巧,它比console.log有何优势,如何使用断点调试定位问题,通过本文,我们将深入解答这些问题,带您从理论到实... 目录观点与案例结合观点1观点2观点3观点4观点5高级调试技巧详解实战案例断点调试:定位变量错误性能分

Java实现将HTML文件与字符串转换为图片

《Java实现将HTML文件与字符串转换为图片》在Java开发中,我们经常会遇到将HTML内容转换为图片的需求,本文小编就来和大家详细讲讲如何使用FreeSpire.DocforJava库来实现这一功... 目录前言核心实现:html 转图片完整代码场景 1:转换本地 HTML 文件为图片场景 2:转换 H

Java使用jar命令配置服务器端口的完整指南

《Java使用jar命令配置服务器端口的完整指南》本文将详细介绍如何使用java-jar命令启动应用,并重点讲解如何配置服务器端口,同时提供一个实用的Web工具来简化这一过程,希望对大家有所帮助... 目录1. Java Jar文件简介1.1 什么是Jar文件1.2 创建可执行Jar文件2. 使用java

SpringBoot实现不同接口指定上传文件大小的具体步骤

《SpringBoot实现不同接口指定上传文件大小的具体步骤》:本文主要介绍在SpringBoot中通过自定义注解、AOP拦截和配置文件实现不同接口上传文件大小限制的方法,强调需设置全局阈值远大于... 目录一  springboot实现不同接口指定文件大小1.1 思路说明1.2 工程启动说明二 具体实施2

Java实现在Word文档中添加文本水印和图片水印的操作指南

《Java实现在Word文档中添加文本水印和图片水印的操作指南》在当今数字时代,文档的自动化处理与安全防护变得尤为重要,无论是为了保护版权、推广品牌,还是为了在文档中加入特定的标识,为Word文档添加... 目录引言Spire.Doc for Java:高效Word文档处理的利器代码实战:使用Java为Wo

SpringBoot日志级别与日志分组详解

《SpringBoot日志级别与日志分组详解》文章介绍了日志级别(ALL至OFF)及其作用,说明SpringBoot默认日志级别为INFO,可通过application.properties调整全局或... 目录日志级别1、级别内容2、调整日志级别调整默认日志级别调整指定类的日志级别项目开发过程中,利用日志

Java中的抽象类与abstract 关键字使用详解

《Java中的抽象类与abstract关键字使用详解》:本文主要介绍Java中的抽象类与abstract关键字使用详解,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录一、抽象类的概念二、使用 abstract2.1 修饰类 => 抽象类2.2 修饰方法 => 抽象方法,没有