网上有很多关于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)、二级缓存(L2),一些高端的CPU还有三级缓存(L3)。所有存储的数据都是下一级缓存的一部分。这三种缓存的技术难度和制造成本都相对降低,因此容量也相对增加。那么有了多级缓存之后,程序的执行就变成了:当CPU要读取一段数据时,先从一级缓存中查找,如果没有找到,再从中查找二级缓存,如果还没有找到就从三级缓存或者内存中查找。随着公司做大,老板要管理的事情越来越多,公司的管理部门开始改革,高层、中层、基层管理人员开始出现。分级管理。单核CPU只包含一组L1、L2、L3缓存;如果CPU包含多个核心,即多核CPU,每个核心都有一组L1(甚至有L2)缓存,共享L3(或有L2)缓存。也有很多类型的公司。有些公司只有一个大老板,他一个人说了算。但有些公司有联席总经理、合伙人等机制。单核CPU就好比一个公司只有一个老板,所有的命令都来自于他,所以管理团队一个就够了。多核CPU就像一个由多个合伙人共同创立的公司。那么,需要为每个合伙人设立一套高层管理人员直接领导,而公司的底层员工则由多个合伙人共享。.也有公司不断壮大,开始分拆各个子公司。每个子公司都有多个CPU,之前没有资源共享。互不影响。下图是单CPU双核缓存结构:随着计算机能力的不断提升,开始支持多线程。那么问题来了,我们分别分析一下单线程和多线程对单核CPU和多核CPU的影响。单线程:CPU核心的缓存只能被一个线程访问。缓存是独占的,不会出现访问冲突等问题。单核CPU,多线程:一个进程中的多个线程会同时访问进程中的共享数据。CPU将一块内存加载到缓存中后,不同线程访问同一个物理地址时,会映射到同一个缓存位置,这样即使发生线程切换,缓存也不会失效。但由于任何时候都只能执行一个线程,因此不会出现缓存访问冲突。多核CPU,多线程:每个核心至少有一个一级缓存。当一个进程中有多个线程访问共享内存,而这多个线程分别在不同的核上执行时,每个核都会在自己的Cache中保留一块共享内存的缓冲区。由于多核可以并行,多个线程可能同时写入自己的缓存,各个缓存之间的数据可能不一样。在CPU和主存之间加入缓存,在多线程场景下可能会导致缓存一致性问题,即在多核CPU中,相同数据的缓存内容在每个核自己的缓存中可能不一致。如果公司的订单是连续发出的,那就没有问题。如果这家公司的订单是并行发布的,而且这些订单都是同一个CEO发布的,这个机制是没有问题的。因为他的订单执行者只有一个管理系统。如果公司的订单都是并行发出的,而这些订单是由多个合作伙伴发出的,那就有问题了。因为每个合伙人只会对自己的直属经理发号施令,而多个经理管理的基层员工可能是共享的。例如,合伙人1要辞退员工a,合伙人2要提拔员工a。升职后,他需要在一次会议上被多个合伙人辞退。两位合伙人分别向各自的经理下达了命令。伙伴1的命令下达后,经理a解雇该员工后,他知道该员工已被解雇。此时合伙人2的经理2在得到消息之前还认为员工a在上班,于是欣然接受了合伙人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.pdfJava内存模型规定所有变量都存储在主存中,而且每个线程也有自己的工作内存。线程中使用的变量的主内存副本保存在线程的工作内存中。线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存。不同的线程不能直接访问对方工作内存中的变量,线程间变量的传递需要自己的工作内存和主存之间进行数据同步。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这两个关键字也可以实现可见性。只是实现方法不同,这里就不展开了。Ordering在Java中,Synchronized和Volatile可以用来保证多线程之间操作的顺序。实现方式不同:Volatile关键字禁止指令重排。Synchronized关键字确保一次只允许一个线程运行。好了,这里简单介绍一下Java并发编程中可以用来解决原子性、可见性和顺序性的关键字。读者可能已经注意到,Synchronized关键字似乎是最好的,它可以同时满足以上三个特性,这也是很多人滥用Synchronized的原因。但是,Synchronized对性能的影响更大。虽然编译器提供了很多锁优化技术,但不建议过度使用。小结看完这篇文章,相信你应该明白什么是Java内存模型,Java内存模型的作用,Java中的内存模型是干什么的。作者:Hollis简介:专栏作家,知名技术博主,个人博客(http://www.hollishuang.com)技术文章阅读量达百万。个人公众号hollis(ID:hollishuang),专注于分享原创Java相关技术内容。
