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

相关文章

一文详解如何在idea中快速搭建一个Spring Boot项目

《一文详解如何在idea中快速搭建一个SpringBoot项目》IntelliJIDEA作为Java开发者的‌首选IDE‌,深度集成SpringBoot支持,可一键生成项目骨架、智能配置依赖,这篇文... 目录前言1、创建项目名称2、勾选需要的依赖3、在setting中检查maven4、编写数据源5、开启热

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

Java对异常的认识与异常的处理小结

《Java对异常的认识与异常的处理小结》Java程序在运行时可能出现的错误或非正常情况称为异常,下面给大家介绍Java对异常的认识与异常的处理,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参... 目录一、认识异常与异常类型。二、异常的处理三、总结 一、认识异常与异常类型。(1)简单定义-什么是

SpringBoot项目配置logback-spring.xml屏蔽特定路径的日志

《SpringBoot项目配置logback-spring.xml屏蔽特定路径的日志》在SpringBoot项目中,使用logback-spring.xml配置屏蔽特定路径的日志有两种常用方式,文中的... 目录方案一:基础配置(直接关闭目标路径日志)方案二:结合 Spring Profile 按环境屏蔽关

Java使用HttpClient实现图片下载与本地保存功能

《Java使用HttpClient实现图片下载与本地保存功能》在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求,其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都... 目录引言一、Apache HttpClient简介二、技术栈与环境准备三、实现图片下载与保存功能1.

SpringBoot排查和解决JSON解析错误(400 Bad Request)的方法

《SpringBoot排查和解决JSON解析错误(400BadRequest)的方法》在开发SpringBootRESTfulAPI时,客户端与服务端的数据交互通常使用JSON格式,然而,JSON... 目录问题背景1. 问题描述2. 错误分析解决方案1. 手动重新输入jsON2. 使用工具清理JSON3.

java中long的一些常见用法

《java中long的一些常见用法》在Java中,long是一种基本数据类型,用于表示长整型数值,接下来通过本文给大家介绍java中long的一些常见用法,感兴趣的朋友一起看看吧... 在Java中,long是一种基本数据类型,用于表示长整型数值。它的取值范围比int更大,从-922337203685477

java Long 与long之间的转换流程

《javaLong与long之间的转换流程》Long类提供了一些方法,用于在long和其他数据类型(如String)之间进行转换,本文将详细介绍如何在Java中实现Long和long之间的转换,感... 目录概述流程步骤1:将long转换为Long对象步骤2:将Longhttp://www.cppcns.c

SpringBoot集成LiteFlow实现轻量级工作流引擎的详细过程

《SpringBoot集成LiteFlow实现轻量级工作流引擎的详细过程》LiteFlow是一款专注于逻辑驱动流程编排的轻量级框架,它以组件化方式快速构建和执行业务流程,有效解耦复杂业务逻辑,下面给大... 目录一、基础概念1.1 组件(Component)1.2 规则(Rule)1.3 上下文(Conte

SpringBoot服务获取Pod当前IP的两种方案

《SpringBoot服务获取Pod当前IP的两种方案》在Kubernetes集群中,SpringBoot服务获取Pod当前IP的方案主要有两种,通过环境变量注入或通过Java代码动态获取网络接口IP... 目录方案一:通过 Kubernetes Downward API 注入环境变量原理步骤方案二:通过