当前位置: 首页 > 科技观察

如果有人问你Java内存模型是什么,给他发这篇文章

时间:2023-03-20 01:36:09 科技观察

前几天发了一篇文章介绍JVM内存结构、Java内存模型、Java对象模型的区别。有很多小伙伴反馈,希望深入讲解每个知识点。Java内存模型是三个知识点中最晦涩的一个,涉及到很多背景知识和相关知识。网上关于Java内存模型的文章很多,在《深入理解Java虚拟机》、《Java并发编程的艺术》等书籍中也有对这个知识点的介绍。但是,很多人看完之后还是一头雾水,甚至有人说自己更加一头雾水。在这篇文章中,我将从整体上介绍Java内存模型。目的很简单。看完这篇文章,你就会知道什么是Java内存模型,为什么会有Java内存模型,Java内存模型解决了哪些问题。这篇文章中,有很多定义和说法,都是作者自己理解后定义的。希望读者能够对Java内存模型有更清晰的认识。当然,如有不妥之处,欢迎指正。为什么会有内存模型在介绍Java内存模型之前,我们先了解一下计算机内存模型是什么,然后再看看Java内存模型在计算机内存模型的基础上做了什么。要说计算机的内存模型,还得说一段古老的历史,看看为什么会有内存模型。记忆模型,英文名MemoryModel,他是一个非常古老的古董。他是一个与计算机硬件相关的概念。那么我先给大家介绍一下它跟硬件有什么关系。CPU与缓存的一致性我们都应该知道,计算机在执行程序时,每条指令都是在CPU中执行的,而在执行的时候,就免不了要和数据打交道。计算机上的数据存储在主存中,也就是计算机的物理内存。一开始还好,但是随着CPU技术的发展,CPU的执行速度越来越快。由于内存的技术没有太大变化,从内存读写数据的过程与CPU的执行速度之间的差距会越来越大,导致CPU每次运行都会消耗更多的内存。很多等待时间。这就像一家初创公司。一开始,创始人和员工的工作关系还算融洽,但随着创始人能力和野心的增长,他和员工之间逐渐出现了鸿沟,普通员工跟不上。总裁的脚步。老板的每一个命令传到基层员工之后,由于基层员工的理解力和执行力都不够,所以要花很多时间。这也无形中拖慢了整个公司的工作效率。但是不能因为内存的读写速度慢就停止发展CPU技术。不可能让内存成为计算机处理的瓶颈。因此,人们想出了一个好办法,就是在CPU和内存之间增加缓存。缓存的概念大家都知道,就是保存一份数据。他的特点是速度快,内存小,价格昂贵。那么,程序的执行过程就变成了:程序在运行时,会将计算所需的数据从主存中复制一份到CPU的缓存中,CPU在执行计算时可以直接从其缓存中读取.读取数据并向其写入数据,当操作结束时,将缓存中的数据刷新到主存中。之后,公司开始设置中层管理人员,直接受CEO领导。领导有什么指示,他直接跟经理们说,然后他就可以忙自己的事了。经理负责协调下级员工的工作。因为管理者了解他们手下的人以及他们的职责。因此,很多时候,CEO只需要与管理人员沟通公司的各种决策和通知即可。随着CPU能力的不断提升,一层缓存逐渐不能满足要求,逐渐衍生出多层缓存。根据数据读取的顺序和与CPU的结合程度,CPU缓存可以分为一级缓存(L1)和二级缓存(L3)。一些高端CPU还具有三级缓存(L3)。所有存储的数据都是下一级缓存的一部分。这三种缓存的技术难度和制造成本都相对降低,因此容量也相对增加。那么有了多级缓存之后,程序的执行就变成了:当CPU要读取一段数据时,先从一级缓存中查找,如果没有找到,再从中查找二级缓存,如果还没有找到就从三级缓存或者内存中查找。随着公司做大,老板要管理的事情越来越多,公司的管理部门开始改革,高层、中层、基层管理人员开始出现。分级管理。单核CPU只包含一组L1、L2、L3缓存;如果CPU包含多个核心,即多核CPU,每个核心都有一组L1(甚至有L2)缓存,共享L3(或有L2)缓存。也有很多类型的公司。有些公司只有一个大老板,他一个人说了算。但有些公司有联席总经理、合伙人等机制。单核CPU就好比一个公司只有一个老板,所有的命令都来自于他,所以管理团队一个就够了。多核CPU就像一个由多个合伙人共同创立的公司。那么,需要为每个合伙人设立一套高层管理人员直接领导,而公司的底层员工则由多个合伙人共享。.也有一些公司不断发展壮大,并开始将其子公司差异化。每个子公司都有多个CPU,之前没有资源共享。互不影响。下图是单CPU双核缓存结构。随着计算机能力的不断提高,开始支持多线程。那么问题来了。下面分别分析一下单线程和多线程对单核CPU和多核CPU的影响。单线程。cpucore的cache只有一个线程访问。缓存是独占的,不会出现访问冲突等问题。单核CPU,多线程。进程中的多个线程会同时访问进程中的共享数据。CPU加载一块内存到缓存后,不同线程访问同一个物理地址时,会映射到同一个缓存位置,这样即使发生线程切换,缓存依然不会失效。但由于任何时候都只能执行一个线程,因此不会出现缓存访问冲突。多核CPU,多线程。每个核心至少有一个一级缓存。如果进程中有多个线程访问某个共享内存,而这多个线程分别在不同的核上执行,每个核都会在自己的caehe中预留一块共享内存缓冲区。由于多核可以并行,多个线程可能同时写入自己的缓存,各个缓存之间的数据可能不一样。在CPU和主存之间加入缓存,在多线程场景下可能会造成缓存一致性问题,即在多核CPU中,在每个核自己的缓存中,相同数据的缓存内容可能不一致。如果公司的订单是连续发出的,那就没有问题。如果这家公司的订单是并行发布的,而且这些订单都是同一个CEO发布的,这个机制是没有问题的。因为他的订单执行者只有一个管理系统。如果公司的订单都是并行发出的,而这些订单是由多个合作伙伴发出的,那就有问题了。因为每个合伙人只会对自己的直属经理发号施令,而多个经理管理的基层员工可能是共享的。例如,合伙人1要辞退员工a,合伙人2要提拔员工a。升职后,他需要在一次会议上被多个合伙人辞退。两位合伙人分别向各自的经理下达了命令。合伙人1下达命令后,经理A解雇该员工后,他知道该员工已被解雇。此时合伙人2的经理2在得到消息之前还认为员工a在上班,于是欣然接受了合伙人给他的升职令。处理器优化和指令重排上文提到,在CPU和主存之间增加缓存,在多线程场景下会出现缓存一致性问题。除了这种情况,还有一个硬件问题也比较重要。也就是说,为了尽可能充分利用处理器内部的运算单元,处理器可以对输入的代码进行乱序执行处理。这是处理器优化。除了很多流行处理器的优化和乱序处理,很多编程语言的编译器也有类似的优化。例如,Java虚拟机的即时编译器(JIT)也会进行指令重排。可以想象,如果任由处理器优化和编译器重新排列指令,可能会导致各种问题。至于员工组织的调整,如果任由人事部门接到多份订单后,随意拆分执行或重新安排,那么对这名员工和这家公司的影响是非常大的。并发编程的问题你可能对前面提到的硬件相关的概念有点迷惑,不知道它和软件有什么关系。但是你应该了解一些关于并发编程的问题,比如原子性问题、可见性问题和顺序问题。其实就是原子性问题、可见性问题和顺序问题。它是由人们抽象定义的。这种抽象的底层问题就是前面提到的缓存一致性问题、处理器优化问题和指令重排问题。这里就这三个问题做一个简单的回顾,我不打算深究了。感兴趣的读者可以自行学习。我们说在并发编程中,为了保证数据的安全,需要满足以下三个特性:原子性是指在一个操作中,CPU不能中途挂起,然后重新调度。如果没有,则不会执行。可见性是指当多个线程访问同一个变量时,一个线程修改变量的值,其他线程可以立即看到修改后的值。顺序是指程序执行的顺序是按照代码的先后顺序执行的。有没有发现缓存一致性问题其实是可见性问题。处理器优化可能会导致原子性问题。指令的重新排序会导致排序问题。因此,下面的文章将不再提及硬件层面的那些概念,而是直接使用熟悉的原子性、可见性和有序性。什么是内存模型?如前所述,缓存一致性问题和处理器优化指令重排问题是由不断的硬件升级引起的。那么,有没有什么机制可以很好的解决上述问题呢?最简单直接的办法就是取消处理器和处理器优化技术,取消CPU缓存,让CPU直接与主存交互。但是这样做可以保证多线程下的并发问题。不过,这样会噎着,有点浪费粮食。因此,为了保证在并发编程中能够满足原子性、可见性和有序性。有一个重要的概念,就是——内存模型。为了保证共享内存的正确性(可见性、顺序性和原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。这些规则用来规范对内存的读写操作,以保证指令执行的正确性。它与处理器有关,与缓存有关,与并发性有关,与编译器有关。解决了CPU多级缓存、处理器优化、指令重排等带来的内存访问问题,保证了并发场景下的一致性、原子性和顺序性。内存模型主要通过两种方式解决并发问题:限制处理器优化和使用内存屏障。本文不会深入底层原理进行介绍,有兴趣的朋友可以自行学习。什么是Java内存模型前面介绍过计算机内存模型,它是解决多线程场景下并发问题的重要规范。那么具体实现是怎样的呢?不同的编程语言可能有不同的实现。我们知道Java程序需要运行在Java虚拟机上。Java内存模型(JMM)是一种内存模型规范,它屏蔽了各种硬件和操作系统之间的访问差异,确保Java程序在各种平台上访问内存时能够保证效果一致的机制和规范。说到Java内存模型,一般是指JDK5使用的新内存模型,主要由JSR-133:JavaTMMemoryModelandThreadSpecification描述。有兴趣的可以参考这个PDF文档(http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf)。Java内存模型规定所有的变量都存放在主存中。一个线程也有自己的工作内存,里面存放着线程中使用的变量的主内存副本。线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存。记忆。不同的线程不能直接访问对方工作内存中的变量,线程间变量的传递需要自己的工作内存和主存之间进行数据同步。JMM作用于工作内存和主内存之间的数据同步过程。他规定了如何做数据同步,什么时候做数据同步。这里所说的主存和工作内存,可以简单类比计算机内存模型中主存和缓存的概念。特别需要注意的是,主内存和工作内存与JVM内存结构中的Java堆、栈、方法区等不是同一层次的内存划分,不能直接比较。《深入理解Java虚拟机》认为,如果一定要勉强对应,从变量、主存、工作内存的定义来看,主存主要对应Java堆中的对象实例数据部分。工作内存对应虚拟机栈的一部分。所以,总结一下,JMM是一个规范,目的是解决多线程通过共享内存通信时,本地内存数据不一致,编译器会对代码指令重新排序,处理器乱序执行代码等问题。带来问题。目的是保证并发编程场景中的原子性、可见性和顺序。Java内存模型的实现了解Java多线程的朋友都知道,Java中提供了一系列与并发处理相关的关键字,如volatile、synchronized、final、concurrenpackages等,其实这些都是提供给程序员的一些关键字Java内存模型封装了底层实现。在开发多线程代码时,我们可以直接使用synchronized等关键字来控制并发,而无需关心底层编译器优化、缓存一致性等问题。因此,Java内存模型除了定义了一套规范外,还提供了一系列原语,将底层实现封装起来,供开发者直接使用。本文不打算一一介绍所有关键字的用法,因为关于每个关键字的用法网上有很多资料。读者可自学。这篇文章要介绍的另一个重要的事情是我们前面提到并发编程必须解决原子性、有序性和一致性的问题。让我们看一下Java中用于确保它的方法。原子性在Java中,为了保证原子性,提供了两条高级字节码指令monitorenter和monitorexit。在synchronized的实现原理一文中介绍了Java中这两个字节码对应的关键字是synchronized。因此在Java中可以使用synchronized来保证方法和代码块内的操作是原子的。VisibilityJava内存模型是通过在变量修改后将新值同步回主存,并在读取变量前从主存中刷新变量值来实现的,它依赖于主存作为传输介质。Java中的volatile关键字提供了一个功能,就是被它修饰的变量在被修改后可以立即同步到主存,被它修饰的变量在每次使用前从主存中刷新。因此可以使用volatile来保证多线程运行时变量的可见性。除了volatile,Java中的synchronized和final这两个关键字也可以实现可见性。只是实现方法不同,这里就不展开了。排序在Java中,可以使用synchronized和volatile来保证多线程之间的操作顺序。实现方式不同:volatile关键字禁止指令重排。synchronized关键字确保同一时间只允许一个线程运行。好了,这里简单介绍一下Java并发编程中可以用来解决原子性、可见性和顺序性的关键字。读者可能已经发现,synchronized关键字似乎是最好的,它可以同时满足以上三个特点,这其实也是很多人滥用synchronized的原因。但是,synchronized对性能的影响更大。虽然编译器提供了很多锁优化技术,但不建议过度使用。小结看完这篇文章,相信你应该明白什么是Java内存模型,Java内存模型的作用,内存模型在Java中的作用。【本文为专栏作家霍利斯原创文章,作者微信公众号Hollis(ID:hollishuang)】点此阅读更多本作者好文