热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入解析Java虚拟机内存模型(JMM)及其核心机制

为了深入理解Java内存模型(JMM),首先需要对计算机硬件体系有全面的认识,尤其是CPU与主存之间的多级缓存架构。这些硬件特性直接影响了JMM的设计和实现,确保在多线程环境下数据的一致性和可见性。

Java内存模型


一、计算机硬件体系


1、CPU多级缓存

要想完全搞清楚Java内存模型,先要了解计算机硬件架构,特别是计算机CPU和主存之间的架构。

在计算机中,cpu和内存的交互最为频繁,相比内存,磁盘读写太慢,内存相当于高速的缓冲区。但是随着cpu的发展,内存的读写速度也远远赶不上cpu。因此cpu厂商在每颗cpu上加上高速多级缓存,用于缓解这种情况。现在cpu和内存的交互大致如下:

img

三级缓存(L1、L2、L3),L1最靠近CPU核心,L2其次,L3再次。

  • 运行速度:L1最快、L2次快、L3最慢;
  • 容量大小:L1最小、L2较大、L3最大。CPU会先在最快的L1中寻找需要的数据,找不到再去找次快的L2,还找不到再去找L3,L3都没有那就只能去内存找了。

​ 其中一级缓存还分为一级数据缓存(Data Cache,D-Cache,L1d)和一级指令缓存(Instruction Cache,I-Cache,L1i),分别用于存放数据及执行数据的指令解码,两者可同时被CPU访问,减少了CPU多核心、多线程争用缓存造成的冲突,提高了处理器的效能。一般CPU的L1i和L1d具备相同的容量。

在这里插入图片描述

2、缓存一致性问题

为什么会出现这个问题呢?

CPU需要修改某个数据,是先去Cache中找,如果Cache中没有找到,再去内存中找,然后把数据复制到Cache中,下次就不需要再去内存中寻找了,然后进行修改操作。而修改操作的过程是先在Cache里面修改数据,然后再把数据刷新到主内存。

其他CPU需要读取数据,也是先去Cache中去寻找,如果找到了就不会去内存找了。 所以当两个CPU的Cache同时都拥有某个数据,其中一个CPU修改了数据,另外一个CPU是无感知的,并不知道这个数据已经不是最新的了,它要读取数据还是从自己的Cache中读取,这样就导致了“缓存不一致”。

3、如何解决 “缓存不一致” ?

解决这个问题的方法有很多,比如:

  • 总线加锁 (此方法性能较低,现在已经不会再使用)
  • MESI协议 : 当一个CPU修改了Cache中的数据,会通知其他缓存了这个数据的CPU,其他CPU会把Cache中这份数据的Cache Line置为无效,要读取数据的话,直接去内存中获取,不会再从Cache中获取了。

在这里插入图片描述

有了 MESI协议 ,我们再来看看多核CPU缓存与主内存的关系。

在这里插入图片描述

二、Java线程与硬件处理器

​ 在Java中开启一个线程,最终也是交给CPU去执行。 具体的流程是:在使用Java线程时,内部会调用操作系统(OS)的内核线程(Kernel-Level Thread),这种线程是操作系统内核(Kernel)直接支持的,内核通过调度器,对线程进行调度,并将线程交给各个CPU内核去处理。 如下图所示:

img

三、Java内存模型


1、Java内存模型概念

Java内存模型(Java Memory Model):JMM规范了Java虚拟机与计算机内存是如何协同工作的,规定了一个线程如何和何时可以看到其他线程修改过的共享变量的值,以及在必须时如何同步的访问共享变量。

JMM是Java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现Java程序在各种不同的平台上都能达到内存访问的一致性。

JMM其实是不存在的,它只是一个规范, 最终Java程序都会交给CPU去运行,所以上面是计算机硬件体系是基础,有了上面的基础,才有了Java内存模型,或者说Java的内存模型就是利用了计算机硬件体系。

本地内存: 我们知道,Java里面每个线程都有一个自己的本地内存(上图绿色区域),存放的是私有变量和主内存数据的副本。如果私有变量是基本数据类型,则直接存放在本地内存,如果是引用类型变量,存放的是引用(指针),实际的数据存放在主内存。本地内存是不共享的,只有属于它的线程可以访问。也有好多人把本地内存称之为线程栈或者工作空间

主内存: 存放的是共享的数据,所有线程都可以访问。当然它也有不少其他称呼,比如堆内存,共享内存等等。

Java内存模型规定了所有对共享变量的读写操作都必须在本地内存中进行,需要先从主内存中拿到数据,复制到本地内存,然后在本地内存中对数据进行修改,再刷新回主内存。

2、Java内存模型和计算机硬件架构之间的关系

​ Java的执行最终还是会交给CPU去处理,但是Java的内存模型和硬件架构又不完全一致。对于硬件来说,只有CPU,Cache和主内存,并没有Java内存模型中本地内存(线程栈、工作空间)或者主内存(共享内存,堆内存)的概念。

img

所以不管是Java内存模型中的本地内存,还是主内存的数据,最终都会存储在CPU(更准确的来说是寄存器)、Cache、内存上。

所以,Java内存模型和计算机硬件架构存在这样的关系:

img

**Java内存模型就是为了解决多线程对共享数据的读写一致性问题。 **

img

3、Java内存模型的同步操作与规则

在这里插入图片描述

(1)JAVA内存模型的同步八种操作

  • Lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状;
  • Unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定。
  • Read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,便于后面的load动作使用。
  • Load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • Use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎。
  • assign((赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量。
  • Store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • Write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

(2)JAVA内存模型的同步规则

  • 如果要把一个变量从主内存中复制到工作内存中,就需要顺序的执行read和load操作,如果把变量从工作内存同步到主内存中,就需要顺序的执行store和write操作。但Java内存模型只要求上述操作必须是按顺序执行,而没有要求是连续执行。
  • 不允许read和load、 store和write操作之一单独出现
  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因的(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即对一个变量实施use和store操作之前,必须先执行assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但是lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值。
  • 如果一个变量事先没有被lock操作,则不允许对它执行unlock。也不允许unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

4、Java并发内存模型的实质

Java内存模型的同步操作与规则就是围绕着Java并发过程中如何处理原子性、可见性和顺序性这三个特征来设计的 。

(1)原子性

不可分割,同生共死。

原子性的关键词: atomic包、CAS、CAS的ABA问题、 LongAdder和AtomicLong的区别比较、 synchronized、lock

(2)可见性

一个线程在本地内存中修改了共享内存的数据,对于其他持有该数据的线程是“不可见”的。

导致共享变量在线程间不可见的原因

  • 线程交叉执行
  • 重排序结合线程交叉执行
  • 共享变量更新后的值没有在工作内存与主存间及时更新

可见性关键词: volatile

(3)有序性

代码在运行的时候,执行顺序可能并不是严格从上到下执行的,会进行指令重排。根据CPU流水线作业,一般来说简单的操作会先执行,复杂的操作后执行。
指令重排会有两个规则:

  • as-if-seria:不管怎么重排序,单线程的执行结果不能发生改变。正是由于这个特性,在单线程中,程序员一般无需理会重排序带来的问题。
  • happens-before原则

有序性关键词: 指令重排 、 happens-before

内容参考于:http://791202.com/2020/04/06/java/761/


推荐阅读
  • 阿里巴巴Java后端开发面试:TCP、Netty、HashMap、并发锁与红黑树深度解析 ... [详细]
  • 修复一个 Bug 竟耗时两天?真的有那么复杂吗?
    修复一个 Bug 竟然耗费了两天时间?这背后究竟隐藏着怎样的复杂性?本文将深入探讨这个看似简单的 Bug 为何会如此棘手,从代码层面剖析问题根源,并分享解决过程中遇到的技术挑战和心得。 ... [详细]
  • 通过一张截图深入解析字节跳动的 Java 开发实力
    在与一位来自字节跳动的朋友交流时了解到,根据他们近期招聘Java工程师的经验,大多数候选人往往在工作3年后会遇到一个难以跨越的瓶颈期。这是因为在职业生涯的这个阶段,许多工程师的技术深度和广度已经达到了一定的水平,但要进一步提升则需要更多的挑战和学习机会。字节跳动作为一家技术驱动的公司,通过严格的面试流程和实际项目经验,能够更好地评估候选人的技术水平和发展潜力。 ... [详细]
  • 2019年后蚂蚁集团与拼多多面试经验详述与深度剖析
    2019年后蚂蚁集团与拼多多面试经验详述与深度剖析 ... [详细]
  • JVM参数设置与命令行工具详解
    JVM参数配置与命令行工具的深入解析旨在优化系统性能,通过合理设置JVM参数,确保在高吞吐量的前提下,有效减少垃圾回收(GC)的频率,进而降低系统停顿时间,提升服务的稳定性和响应速度。此外,本文还将详细介绍常用的JVM命令行工具,帮助开发者更好地监控和调优JVM运行状态。 ... [详细]
  • Java队列机制深度解析与应用指南
    Java队列机制在并发编程中扮演着重要角色。本文深入解析了Java队列的各种实现类及其应用场景,包括`LinkedList`、`ArrayBlockingQueue`和`PriorityQueue`等,并探讨了它们在高并发环境下的性能表现和适用场景。通过详细分析这些队列的内部机制和使用技巧,帮助开发者更好地理解和应用Java队列,提升系统的设计和架构能力。 ... [详细]
  • 深入理解Spark框架:RDD核心概念与操作详解
    RDD是Spark框架的核心计算模型,全称为弹性分布式数据集(Resilient Distributed Dataset)。本文详细解析了RDD的基本概念、特性及其在Spark中的关键操作,包括创建、转换和行动操作等,帮助读者深入理解Spark的工作原理和优化策略。通过具体示例和代码片段,进一步阐述了如何高效利用RDD进行大数据处理。 ... [详细]
  • 西北工业大学作为陕西省三所985和211高校之一,虽然在农业和林业领域不如某些顶尖院校,但在航空航天领域的实力尤为突出。该校的计算机科学专业在科研和教学方面也具有显著优势,是考研的理想选择。 ... [详细]
  • 本文深入探讨了IO复用技术的原理与实现,重点分析了其在解决C10K问题中的关键作用。IO复用技术允许单个进程同时管理多个IO对象,如文件、套接字和管道等,通过系统调用如`select`、`poll`和`epoll`,高效地处理大量并发连接。文章详细介绍了这些技术的工作机制,并结合实际案例,展示了它们在高并发场景下的应用效果。 ... [详细]
  • 如何正确配置与使用日志组件:Log4j、SLF4J及Logback的连接与整合方法
    在当前的软件开发实践中,无论是开源项目还是日常工作中,日志框架都是不可或缺的工具之一。本文详细探讨了如何正确配置与使用Log4j、SLF4J及Logback这三个流行的日志组件,并深入解析了它们之间的连接与整合方法,旨在帮助开发者高效地管理和优化日志记录流程。 ... [详细]
  • IIS 7及7.5版本中应用程序池的最佳配置策略与实践
    在IIS 7及7.5版本中,优化应用程序池的配置是提升Web站点性能的关键步骤。具体操作包括:首先定位到目标Web站点的应用程序池,然后通过“应用程序池”菜单找到对应的池,右键选择“高级设置”。在一般优化方案中,建议调整以下几个关键参数:1. **基本设置**: - **队列长度**:默认值为1000,可根据实际需求调整队列长度,以提高处理请求的能力。此外,还可以进一步优化其他参数,如处理器使用限制、回收策略等,以确保应用程序池的高效运行。这些优化措施有助于提升系统的稳定性和响应速度。 ... [详细]
  • ZeroMQ在云计算环境下的高效消息传递库第四章学习心得
    本章节深入探讨了ZeroMQ在云计算环境中的高效消息传递机制,涵盖客户端请求-响应模式、最近最少使用(LRU)队列、心跳检测、面向服务的队列、基于磁盘的离线队列以及主从备份服务等关键技术。此外,还介绍了无中间件的请求-响应架构,强调了这些技术在提升系统性能和可靠性方面的应用价值。个人理解方面,ZeroMQ通过这些机制有效解决了分布式系统中常见的通信延迟和数据一致性问题。 ... [详细]
  • Java中将Map及其他对象高效转换为JSON格式的方法探讨 ... [详细]
  • 深入解析:字符串与对象的对比及应用
    本文深入探讨了字符串与对象在编程中的对比及其应用场景。通过分析字符串作为不可变对象的特性,以及对象在内存中的存储方式,揭示了两者在性能和使用上的差异。文章还详细解析了Python中所有类均继承自`object`类的机制,并介绍了`getClass()`方法的底层实现,强调了`native`关键字的作用。此外,结合实际案例,讨论了在不同场景下选择字符串或对象的最佳实践。 ... [详细]
  • 本文将详细介绍如何利用JMeter高效执行API接口测试,涵盖JMeter的基础介绍、安装方法、中文环境配置、主要元件及其作用域和执行顺序等内容,并分享一系列实用的测试技巧,帮助读者全面掌握JMeter接口测试的全过程。 ... [详细]
author-avatar
a734839433
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有