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
程序次序规则
一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。volatile规则(以后会花一整节内容介绍,这里不展开)
锁定规则
如果锁处于Lock的状态,必须等Unlock后,才能再次进行Lock操作。传递规则
A happens-before B , B happens-before C,那么A happens-before C。
Java内存模型是个相当复杂的东西,我在这里可能还说不上是谈,只能说是“蜻蜓点水 ”般的介绍下。希望通过这篇文章,大家可以对Java模型有一个初步的了解。
以后,我也会介绍Synchronized 和 volatile关键字等等,我可能会再次提到本节中涵盖的内容,并做进一步的补充说明。
好了,本文的内容到这里就结束了,在写之前,已经做好心理准备了,可能需要花上半天时间,但是实际上远远不止半天,在写的过程中,翻阅了大量的文章,包括 知乎、博客园、简书 等等,发现 如果要“较真”“抬杠”的话,文章与文章之间也有有冲突的地方,甚至一篇文章中,也有前后矛盾的地方。我也不奢求本文中介绍的所有内容都是正确的。为了不误人子弟,如果大家发现有错误,希望可以及时向我提出,我也会尽快核实后修改。
感谢大家可以看到最后,再见。