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

相关文章

在Ubuntu上部署SpringBoot应用的操作步骤

《在Ubuntu上部署SpringBoot应用的操作步骤》随着云计算和容器化技术的普及,Linux服务器已成为部署Web应用程序的主流平台之一,Java作为一种跨平台的编程语言,具有广泛的应用场景,本... 目录一、部署准备二、安装 Java 环境1. 安装 JDK2. 验证 Java 安装三、安装 mys

Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单

《Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单》:本文主要介绍Springboot的ThreadPoolTaskScheduler线... 目录ThreadPoolTaskScheduler线程池实现15分钟不操作自动取消订单概要1,创建订单后

JAVA中整型数组、字符串数组、整型数和字符串 的创建与转换的方法

《JAVA中整型数组、字符串数组、整型数和字符串的创建与转换的方法》本文介绍了Java中字符串、字符数组和整型数组的创建方法,以及它们之间的转换方法,还详细讲解了字符串中的一些常用方法,如index... 目录一、字符串、字符数组和整型数组的创建1、字符串的创建方法1.1 通过引用字符数组来创建字符串1.2

SpringCloud集成AlloyDB的示例代码

《SpringCloud集成AlloyDB的示例代码》AlloyDB是GoogleCloud提供的一种高度可扩展、强性能的关系型数据库服务,它兼容PostgreSQL,并提供了更快的查询性能... 目录1.AlloyDBjavascript是什么?AlloyDB 的工作原理2.搭建测试环境3.代码工程1.

Java调用Python代码的几种方法小结

《Java调用Python代码的几种方法小结》Python语言有丰富的系统管理、数据处理、统计类软件包,因此从java应用中调用Python代码的需求很常见、实用,本文介绍几种方法从java调用Pyt... 目录引言Java core使用ProcessBuilder使用Java脚本引擎总结引言python

SpringBoot操作spark处理hdfs文件的操作方法

《SpringBoot操作spark处理hdfs文件的操作方法》本文介绍了如何使用SpringBoot操作Spark处理HDFS文件,包括导入依赖、配置Spark信息、编写Controller和Ser... 目录SpringBoot操作spark处理hdfs文件1、导入依赖2、配置spark信息3、cont

springboot整合 xxl-job及使用步骤

《springboot整合xxl-job及使用步骤》XXL-JOB是一个分布式任务调度平台,用于解决分布式系统中的任务调度和管理问题,文章详细介绍了XXL-JOB的架构,包括调度中心、执行器和Web... 目录一、xxl-job是什么二、使用步骤1. 下载并运行管理端代码2. 访问管理页面,确认是否启动成功

Java中的密码加密方式

《Java中的密码加密方式》文章介绍了Java中使用MD5算法对密码进行加密的方法,以及如何通过加盐和多重加密来提高密码的安全性,MD5是一种不可逆的哈希算法,适合用于存储密码,因为其输出的摘要长度固... 目录Java的密码加密方式密码加密一般的应用方式是总结Java的密码加密方式密码加密【这里采用的

Java中ArrayList的8种浅拷贝方式示例代码

《Java中ArrayList的8种浅拷贝方式示例代码》:本文主要介绍Java中ArrayList的8种浅拷贝方式的相关资料,讲解了Java中ArrayList的浅拷贝概念,并详细分享了八种实现浅... 目录引言什么是浅拷贝?ArrayList 浅拷贝的重要性方法一:使用构造函数方法二:使用 addAll(

解决mybatis-plus-boot-starter与mybatis-spring-boot-starter的错误问题

《解决mybatis-plus-boot-starter与mybatis-spring-boot-starter的错误问题》本文主要讲述了在使用MyBatis和MyBatis-Plus时遇到的绑定异常... 目录myBATis-plus-boot-starpythonter与mybatis-spring-b