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(原子性)

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

一般情况下,在64位操作系统之下,基本数据类型的赋值,读取都是具有原子性的。

可见性

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

有序性

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

  • as-if-seria
    不管怎么重排序,单线程的执行结果不能发生改变。正是由于这个特性,在单线程中,程序员一般无需理会重排序带来的问题。
  • happens-before
    1. 程序次序规则
      一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
    2. volatile规则(以后会花一整节内容介绍,这里不展开)
    3. 锁定规则
      如果锁处于Lock的状态,必须等Unlock后,才能再次进行Lock操作。
    4. 传递规则
      A happens-before B , B happens-before C,那么A happens-before C。

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

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

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

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

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

  1. 浅谈JVM内存模型

    JAVA虚拟机在执行JAVA程序的时候,会把它管理的内存分成若干不同的数据区域,每个区域都有各自的用途.目前大致把JVM内存模型划分为五个区域:程序计数器,虚拟机栈,本地方法栈,堆和方法区. 程序计数 ...

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

    一.导论 java技术体系中所提到的内存自动化管理归根结底就是内存的分配与回收两个问题,之前已经和大家谈过java回收的相关知识,今天来和大家聊聊java对象的在内存中的分配.通俗的讲,对象的内存分配 ...

  3. 浅谈Java内存及GC

    目录: 1.JAVA虚拟机规范与JAVA虚拟机 2.JVM结构图 3.GC策略与原理 4.垃圾收集算法 5.回收的时机 6.垃圾搜集器 一.JAVA虚拟机规范与JAVA虚拟机 内存,是指程序运行时的数 ...

  4. 浅谈java内存泄漏

    最近有朋友遇到个问题,tomcat在运行几天后就会报outofmemory,然后就死了,我就稍微总结了下内存泄漏的一些原因,纯属个人理解,欢迎大侠们劈砖: 一.字符串问题 这个也是一个常见的问题,我们 ...

  5. 浅谈Java内存泄露

    一.引言 先等等吧……累了

  6. 2015第27周三Java内存模型

    自己写的代码,6个月不看也是别人的代码,自己学的知识也同样如此,学完的知识如果不使用或者不常常回顾,那么还不是自己的知识. 要认识java线程安全,必须了解两个主要的点:java的内存模型,java的 ...

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

    本文的内存模型只写虚拟机内存模型,物理机的不予描述. Java内存模型 在Java中,虚拟机将运行时区域分成6中,如下图:              程序计数器:用来记录当前线程执行到哪一步操作.在多 ...

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

    在大型项目编码推进中,涉及到 XML 解析问题时,大多数程序员都不太会选用底层的解析方式直接编码. 主要存在编码复杂性.难扩展.难复用....,但如果你是 super 程序员或是一个人的项目,也不妨一 ...

  9. 浅谈Java线程安全

    浅谈Java线程安全 - - 2019-04-25    17:37:28 线程安全 Java中的线程安全 按照线程安全的安全程序由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下五类 ...

随机推荐

  1. mybatis和hibernate中的懒加载

    概念:所谓懒加载就是延时加载,延迟加载.什么时候用懒加载呢,我只能回答要用懒加载的时候就用懒加载.至于为什么要用懒加载呢,就是当我们要访问的数据量过大时,明显用缓存不太合适,因为内存容量有限 ,为了减 ...

  2. python3 代理设置

    前言: 用过很多种代理使用方式,这里总结一下. 1.urllib+socks5的代理1 from sockshandler import SocksiPyHandler import socks fr ...

  3. datatable拆分多个

    /// <summary> /// 分解数据表 /// </summary> /// <param name="originalTab">需要分 ...

  4. 大数据ssh疑点跟踪

    相信运维的对ssh免密登陆应该是对这个再清楚不过的吧,由于我们大数据对于安全这方便管控的很严格,单独找一台物理机作为跳板机,其他的机器都必须要从这个跳板机免密登陆,由于机器比较的多,其中dn30这个域 ...

  5. bower 基础认识

    bower 跟 npm 很像 都是管理包的工具 只是 bower 偏向前端  npm 前后都能管理 npm init 后是  生产 package.json  下载的包在node_modules文件下 ...

  6. UOJ#374. 【ZJOI2018】历史 贪心,LCT

    原文链接https://www.cnblogs.com/zhouzhendong/p/UOJ374.html 题解 想出正解有点小激动. 不过因为傻逼错误调到自闭.不如贺题 首先我们考虑如何 $O(n ...

  7. 在本机eclipse中创建maven项目,查看linux中hadoop下的文件、在本机搭建hadoop环境

    注意 第一次建立maven项目时需要在联网情况下,因为他会自动下载一些东西,不然突然终止 需要手动删除断网前建立的文件 在eclipse里新建maven项目步骤 直接新建maven项目出了错      ...

  8. HDU-AcmKeHaoWanLe训练实录

    菜鸡队训练实录. 现场赛记录:[名称:奖项/排名] 2017: ICPC Shenyang:Gold/3 CCPC Hangzhou:Gold/3 ICPC Beijing:Gold/13 CCPC ...

  9. 最短路径问题—Dijkstra算法

    算法: import java.util.*; public class Main6 { public static int N = 1050; public static final int INF ...

  10. Android找回密码功能 手机找回、邮箱找回

    找回密码功能设计:https://blog.csdn.net/qq_33472765/article/details/82287404?utm_source=blogxgwz0 手机找回:https: ...