浅谈Java内存模型

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

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

计算机硬件体系

咱们都知道CPU 和 内存是计算机中比较核心的两个东西,它们之间会频繁的交互,随着CPU发展愈来愈快,内存的读写的速度远远不如CPU的处理速度,因此CPU厂商在CPU上加了一个 高速缓存,用来缓解这种问题。咱们在看CPU硬件参数的时候,也会看到有这样的参数:
image.png
通常高速缓存有3级:L1,L2,L3,CPU与内存的交互,就发生了变化,CPU再也不与内存直接交互,CPU会先去L1中寻找数据,没有的话,再去L2中寻找,而后是L3,最后才去内存寻找(更准确的来讲,应该是CPU中的寄存器去寻找)。面试

咱们能够画一张图来理解:
image.png编程

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

这样就会引起一个问题:缓存不一致多线程

为何会出现这个问题呢?架构

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

因此当两个CPU的Cache同时都拥有某个数据,其中一个CPU修改了数据,另一个CPU是无感知的,并不知道这个数据已经不是最新的了,它要读取数据仍是从本身的Cache中读取,这样就致使了“缓存不一致”。app

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

解决这个问题的方法有不少,好比:

  • 总线加锁(此方法性能较低,如今已经不会再使用)
  • MESI协议
    这是Intel提出的,MESI协议也是至关复杂,在这里我就简单的说下:当一个CPU修改了Cache中的数据,会通知其余缓存了这个数据的CPU,其余CPU会把Cache中这份数据的Cache Line置为无效,要读取数据的话,直接去内存中获取,不会再从Cache中获取了。

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

Java线程与硬件处理器

其实,咱们在Java中开启一个线程,最终Java也会交给CPU去执行。
具体的流程是:咱们在使用Java线程,内部会调用操做系统(OS)的内核线程(Kernel-Level Thread),这种线程是操做系统内核(Kernel)直接支持的,内核经过调度器,对线程进行调度,并将线程交给各个CPU内核去处理。

以下图所示:
image.png

Java内存模型

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

仍是从一张图来入手:
image.png

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

主内存:存放的是共享的数据,全部线程均可以访问。固然它也有很多其余称呼,好比 堆内存,共享内存等等。

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

经过前面的铺垫,咱们应该认识到Java的执行最终仍是会交给CPU去处理,可是Java的内存模型和硬件架构又不彻底一致。对于硬件来讲,只有CPU,Cache和主内存,并无Java内存模型中本地内存(线程栈、工做空间)或者主内存(共享内存,堆内存)的概念,因此不论是Java内存模型中的本地内存,仍是主内存的数据,最终都会存储在CPU(更准确的来讲 是寄存器)、Cache、内存上。

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

image.png

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关键字等等,我可能会再次提到本节中涵盖的内容,并作进一步的补充说明。

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

感谢你们能够看到最后,再见。

相关文章
相关标签/搜索