浅谈Java内存模型

Java内存模型虽说是一个老生常谈的问题 ,也是大厂面试中绕不过的,甚至初级面试也会问到。但是真正要理解起来,还是相当困难,主要这个东西看不见,摸不着。网上已经有大量的博客,但是人家的终究是人家的,自己也要好好的去理解,去消化。今天我也来班门弄斧,说下Java内存模型。

说到Java内存模型,不得不说到 计算机硬件方面的知识。

计算机硬件体系

我们都知道CPU 和 内存是计算机中比较核心的两个东西,它们之间会频繁的交互,随着CPU发展越来越快,内存的读写的速度远远不如CPU的处理速度,所以CPU厂商在CPU上加了一个 高速缓存,用来缓解这种问题。我们在看CPU硬件参数的时候,也会看到有这样的参数:

一般高速缓存有3级:L1,L2,L3,CPU与内存的交互,就发生了变化,CPU不再与内存直接交互,CPU会先去L1中寻找数据,没有的话,再去L2中寻找,然后是L3,最后才去内存寻找(更准确的来说,应该是CPU中的寄存器去寻找)。

我们可以画一张图来理解:

看起来一切都很美好,但是随着科技的进步,CPU厂商们叒搞事了,推出了多核CPU,每个CPU上又有高速缓存,CPU与内存的交互就变成了下面这个样子:

这样就会引发一个问题:缓存不一致

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

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

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

其实对于这样的描述并不是十分准确,因为计算、读取等操作都是在CPU的寄存器中进行的,这样的描述是为了让问题变得更简单,相信学过计算机体系的人应该非常清楚整个流程,在这里就简单的描述下。

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

  • 总线加锁(此方法性能较低,现在已经不会再使用)
  • MESI协议

    这是Intel提出的,MESI协议也是相当复杂,在这里我就简单的说下:当一个CPU修改了Cache中的数据,会通知其他缓存了这个数据的CPU,其他CPU会把Cache中这份数据的Cache Line置为无效,要读取数据的话,直接去内存中获取,不会再从Cache中获取了。

当然还有其他的解决方案,MESI协议是其中比较出名的。

Java线程与硬件处理器

其实,我们在Java中开启一个线程,最终Java也会交给CPU去执行。

具体的流程是:我们在使用Java线程,内部会调用操作系统(OS)的内核线程(Kernel-Level Thread),这种线程是操作系统内核(Kernel)直接支持的,内核通过调度器,对线程进行调度,并将线程交给各个CPU内核去处理。

如下图所示:

Java内存模型

看到标题,大家肯定会想:我靠,难道上面说的都和Java内存模型没有关系吗,从这里才是真正介绍Java内存模型吗?其实,并不是,Java内存模型是一个抽象的概念,其实并不存在,它描述的是一种规范,最终Java程序都会交给CPU去运行,所以上面是计算机硬件体系是基础,有了上面的基础,才有了Java内存模型,或者说Java的内存模型就是利用了计算机硬件体系。

还是从一张图来入手:

本地内存:存放的是 私有变量 和 主内存数据的副本。如果私有变量是基本数据类型,则直接存放在本地内存,如果是引用类型变量,存放的是引用(指针),实际的数据存放在主内存。本地内存是不共享的,只有属于它的线程可以访问。也有好多人把 本地内存 称之为 线程栈 或者 工作空间。

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

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

通过前面的铺垫,我们应该认识到Java的执行最终还是会交给CPU去处理,但是Java的内存模型和硬件架构又不完全一致。对于硬件来说,只有CPU,Cache和主内存,并没有Java内存模型中本地内存(线程栈、工作空间)或者主内存(共享内存,堆内存)的概念,所以不管是Java内存模型中的本地内存,还是主内存的数据,最终都会存储在CPU(更准确的来说 是寄存器)、Cache、内存上。

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

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

并发编程中三个重要特性

原子性:

不可分割,同生共死。

i=1

具有原子性,直接 在本地内存中进行赋值操作。

i++;

不具有原子性,有三个步骤

1.把i读取出来(原子性)

2.在本地内存中做自增运算(原子性)

3.再把值写回i(原子性)

多个原子性操作组合在一起,就不具有原子性了。

一般情况下,基本数据类型的赋值,读取都是具有原子性的。

可见性

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

有序性

代码在运行的时候,执行顺序可能并不是严格从上到下执行的,会进行指令重排。

根据CPU流水线作业,一般来说 简单的操作会先执行,复杂的操作后执行。

指令重排会有两个规则:

  • as-if-seria

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

  • happens-before
    1. 程序次序规则

      一段代码在单线程中执行的结果是有序的(感觉就是as-if-seria原则)

    2. volatile规则(以后会花一整节内容介绍,这里不展开)
    3. 锁定规则

      如果锁处于Lock的状态,必须等Unlock后,才能再次进行Lock操作。

    4. 传递规则

      A happens-before B , B happens-before C,那么A happens-before C。

Java内存模型是个相当复杂的东西,我在这里可能还说不上是谈,只能说是“蜻蜓点水 ”般的介绍下。希望通过这篇文章,大家可以对Java模型有一个初步的了解。

以后,我也会介绍Synchronized 和 volatile关键字等等,我可能会再次提到本节中涵盖的内容,并做进一步的补充说明。

好了,本文的内容到这里就结束了,在写之前,已经做好心理准备了,可能需要花上半天时间,但是实际上远远不止半天,在写的过程中,翻阅了大量的文章,包括 知乎、博客园、简书 等等,发现 如果要“较真”“抬杠”的话,文章与文章之间也有有冲突的地方,甚至一篇文章中,也有前后矛盾的地方。我也不奢求本文中介绍的所有内容都是正确的。为了不误人子弟,如果大家发现有错误,希望可以及时向我提出,我也会尽快核实后修改。

感谢大家可以看到最后,再见。

原文地址:https://www.cnblogs.com/CodeBear/p/10129159.html

时间: 2024-10-07 00:52:40

浅谈Java内存模型的相关文章

由volatile关键字谈Java内存模型

volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情.由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用volatile关键字的场景 1. 内存模型的相关概念 当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再

浅谈java内存分配和回收策略

一.导论 java技术体系中所提到的内存自动化管理归根结底就是内存的分配与回收两个问题,之前已经和大家谈过java回收的相关知识,今天来和大家聊聊java对象的在内存中的分配.通俗的讲,对象的内存分配就是在堆上的分配,对象主要分配在新生代的Eden上(关于对象在内存上的分代在垃圾回收中会补上,想了解的也可以参考<深入理解java虚拟机>),如果启动了本地线程分配缓冲,讲按线程优先在TLAB上分配.少数情况下也是直接在老年代中分配. 二.经典的分配策略 1.对象优先在Eden上分配 一般情况下对

浅谈Java内存管理(二)

我们几乎无法从Java本身改变其回收机制的策列,但我们可以改变我们的编程方式和在编程中的注意事项. 1.Java没有C++中对象析构的功能,但Java的垃圾回收机制是有原则的,它会回收没有变量引用的对象.这种对象没有变量引用它,也就再也不可能有任何方法找到这个“丢失的”对象了——GC回收的就是这种对象.所以当我们确定一个对象不再有任何价值和意义时,将这个对象对应的所有变量引用全部置为NULL,这时这个对象就符合了GC的回收标准.对于我们而言也就相当于手动做了对象析构的操作,至于是否真的回收,那是

浅谈Java内存管理(一)

提到Java的内存管理,我始终抱有一种又爱又恨的心理.作为一门面向对象的高级语言,Java的确为减轻程序员的负担做出了巨大的努力,它的垃圾回收机制帮助百万程序员从C系语言恼人的内存管理问题中解脱出来,成为自身的一大亮点:但同时,垃圾回收机制的不可强制执行的特点,又让了解过C/C++的人觉得自动垃圾回收反而是一种累赘,自己有管理内存的能力也不能施展,在一定程度上又限制了程序员的发挥.然而,Java又不可能因为这点就放弃自己的垃圾自动回收机制,所以,能改变的只有我们自己了. “理想”的Java垃圾回

对java内存模型的认识

浅谈java内存模型        不同的平台,内存模型是不一样的,但是jvm的内存模型规范是统一的.其实java的多线程并发问题最终都会反映在java的内存模型上,所谓线程安全无非是要控制多个线程对某个资源的有序访问或修改.总结java的内存模型,要解决两个主要的问题:可见性和有序性.我们都知道计算机有高速缓存的存在,处理器并不是每次处理数据都是取内存的.JVM定义了自己的内存模型,屏蔽了底层平台内存管理细节,对于java开发人员,要清楚在jvm内存模型的基础上,如果解决多线程的可见性和有序性

2015第27周三Java内存模型

自己写的代码,6个月不看也是别人的代码,自己学的知识也同样如此,学完的知识如果不使用或者不常常回顾,那么还不是自己的知识. 要认识java线程安全,必须了解两个主要的点:java的内存模型,java的线程同步机制.特别是内存模型,java的线程同步机制很大程度上都是基于内存模型而设定的. 浅谈java内存模型        不同的平台,内存模型是不一样的,但是jvm的内存模型规范是统一的.其实java的多线程并发问题最终都会反映在java的内存模型上,所谓线程安全无非是要控制多个线程对某个资源的

浅谈Java的内存模型以及交互

本文的内存模型只写虚拟机内存模型,物理机的不予描述. Java内存模型 在Java中,虚拟机将运行时区域分成6中,如下图:              程序计数器:用来记录当前线程执行到哪一步操作.在多线程轮换的模式中,当当前线程时间片用完的时候记录当前操作到哪一步,重新获得时间片时根据此记录来恢复之前的操作. 虚拟机栈:这就是我们平时所说的栈了,一般用来储存局部变量表.操作数表.动态链接等. 本地方法栈:这是另一个栈,用来提供虚拟机中用到的本地服务,像线程中的start方法,JUC包里经常使用的

浅谈 Java 主流开源类库解析 XML

在大型项目编码推进中,涉及到 XML 解析问题时,大多数程序员都不太会选用底层的解析方式直接编码. 主要存在编码复杂性.难扩展.难复用....,但如果你是 super 程序员或是一个人的项目,也不妨一试. Jdom/Dom4j/Xstream... 基于底层解析方式重新组织封装的开源类库,简洁明了的 API,稳定高效的运行表现. Dom4j 基于 JAXP 解析方式,性能优异.功能强大.极易使用的优秀框架.想了解底层解析方式请翻看:浅谈 Java XML 底层解析方式 Jdom 你细看内部代码,

浅谈 Java Printing

浅谈 Java  Printing 其实怎么说呢?在写这篇博文之前,我对java printing 可以说是一无所知的.以至于我在敲文字时, 基本上是看着api文档翻译过来的.这虽然看起来非常的吃力,但是我相信,有道大哥不会辜负我的.嘻 嘻! Java Printing 技术,也就是我们平时所接触的打印,只不过是说可以用Java实现而已. 一.Java Printing 打印简介 Java Printing API能够使java应用程序实现相关的打印功能,如: 1.打印所有 Java 2D 和