Java内存模型一个大厂面试中绕不过,初级面试也会问到的经典问题。要真正要理解起来,还是困难。今天我和大家说下这Java内存模型。
说到Java内存模型,不得不说到计算机硬件方面的知识。
计算机硬件体系
CPU和内存是计算机中较核心的两个部分,它们之间会频繁的交互,随着CPU发展,内存的读写的速度远远不如CPU的处理速度,所以CPU厂商在CPU上加了一个高速缓存,用来缓解这种问题。我们在看CPU硬件参数的时候,也会看到有这样的参数:
一般高速缓存有3级:L1,L2,L3,CPU与内存的交互,就发生了变化,CPU不再与内存直接交互,CPU会先去L1中寻找数据,没有的话,再去L2中寻找,然后是L3,最后才去内存寻找(更准确的来说,应该是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中获取了。
Java线程与硬件处理器
我们在Java中开启一个线程,最终Java也会交给CPU去执行。
流程:使用Java线程,内部会调用操作系统(OS)的内核线程(Kernel-Level Thread),线程是操作系统内核(Kernel)直接支持,内核通过调度器,对线程进行调度,并将线程交给各个CPU内核去处理。
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。
- 程序次序规则
希望能够帮到你,之后见。