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

相关文章

JavaWeb-WebSocket浏览器服务器双向通信方式

《JavaWeb-WebSocket浏览器服务器双向通信方式》文章介绍了WebSocket协议的工作原理和应用场景,包括与HTTP的对比,接着,详细介绍了如何在Java中使用WebSocket,包括配... 目录一、概述二、入门2.1 POM依赖2.2 编写配置类2.3 编写WebSocket服务2.4 浏

配置springboot项目动静分离打包分离lib方式

《配置springboot项目动静分离打包分离lib方式》本文介绍了如何将SpringBoot工程中的静态资源和配置文件分离出来,以减少jar包大小,方便修改配置文件,通过在jar包同级目录创建co... 目录前言1、分离配置文件原理2、pom文件配置3、使用package命令打包4、总结前言默认情况下,

Java文件与Base64之间的转化方式

《Java文件与Base64之间的转化方式》这篇文章介绍了如何使用Java将文件(如图片、视频)转换为Base64编码,以及如何将Base64编码转换回文件,通过提供具体的工具类实现,作者希望帮助读者... 目录Java文件与Base64之间的转化1、文件转Base64工具类2、Base64转文件工具类3、

java获取图片的大小、宽度、高度方式

《java获取图片的大小、宽度、高度方式》文章介绍了如何将File对象转换为MultipartFile对象的过程,并分享了个人经验,希望能为读者提供参考... 目China编程录Java获取图片的大小、宽度、高度File对象(该对象里面是图片)MultipartFile对象(该对象里面是图片)总结java获取图片

Java通过反射获取方法参数名的方式小结

《Java通过反射获取方法参数名的方式小结》这篇文章主要为大家详细介绍了Java如何通过反射获取方法参数名的方式,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1、前言2、解决方式方式2.1: 添加编译参数配置 -parameters方式2.2: 使用Spring的内部工具类 -

Java如何获取视频文件的视频时长

《Java如何获取视频文件的视频时长》文章介绍了如何使用Java获取视频文件的视频时长,包括导入maven依赖和代码案例,同时,也讨论了在运行过程中遇到的SLF4J加载问题,并给出了解决方案... 目录Java获取视频文件的视频时长1、导入maven依赖2、代码案例3、SLF4J: Failed to lo

如何使用Spring boot的@Transactional进行事务管理

《如何使用Springboot的@Transactional进行事务管理》这篇文章介绍了SpringBoot中使用@Transactional注解进行声明式事务管理的详细信息,包括基本用法、核心配置... 目录一、前置条件二、基本用法1. 在方法上添加注解2. 在类上添加注解三、核心配置参数1. 传播行为(

在Java中使用ModelMapper简化Shapefile属性转JavaBean实战过程

《在Java中使用ModelMapper简化Shapefile属性转JavaBean实战过程》本文介绍了在Java中使用ModelMapper库简化Shapefile属性转JavaBean的过程,对比... 目录前言一、原始的处理办法1、使用Set方法来转换2、使用构造方法转换二、基于ModelMapper

JAVA调用Deepseek的api完成基本对话简单代码示例

《JAVA调用Deepseek的api完成基本对话简单代码示例》:本文主要介绍JAVA调用Deepseek的api完成基本对话的相关资料,文中详细讲解了如何获取DeepSeekAPI密钥、添加H... 获取API密钥首先,从DeepSeek平台获取API密钥,用于身份验证。添加HTTP客户端依赖使用Jav

Java实现状态模式的示例代码

《Java实现状态模式的示例代码》状态模式是一种行为型设计模式,允许对象根据其内部状态改变行为,本文主要介绍了Java实现状态模式的示例代码,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来... 目录一、简介1、定义2、状态模式的结构二、Java实现案例1、电灯开关状态案例2、番茄工作法状态案例