本系列不会有规律的持续更新,而是学到了、接触到了,就会进行整理总结。 学习本章之前,请先学习Java内存的相关基础知识 水平有限,如若有错,感谢指正! 本章参考-1 本章参考-2
Java内存模型是一种抽象的规则或规范,定义了程序中存在竞争现象的对象(包括实例字段、静态字段和数组对象,不包括局部变量,形式参数;后者是线程私有,不存在竞争问题)的访问方式。
如果我们要想深入了解Java并发编程,就要先理解好Java内存模型。Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步。
JVM通过Java内存模型,屏蔽掉不同硬件和操作系统的内存访问差异,实现各种平台具有一致的并发效果。
显然,CPU的运行速度远远大于内存交互速度,至少有这几个数量级的差距。因此,现代计算机在CPU与内存之间都有一个高速缓存(Cache)作为数据缓冲:将运算需要的数据从内存中复制到缓存中,当缓存从CPU中拿到运算结果之后再同步到内存中——这样CPU就不用等待缓慢的内存读写了。
但此时就出现了一个大问题:缓存一致性。
现在基本都是多核处理器,每个处理器都有自己的高速缓存,而他们共享同一个主内存,这样就会导致各自的缓存数据不一致的情况,为了解决这个问题各个处理器访问缓存时都要遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI、MOSI、Synapse等等。
除此之外,为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。类似的,Java虚拟机的即时编译器中也有类似的指令重排序优化。
接下来,我们通过一个Java程序了解缓存一致性。
Java的内存模型跟计算机的内存模型极其相似
CPU在内存模型当中充当的就是“算术逻辑单元”,主要负责运算。
Java线程也可以理解为“运算单元”,因此,可以看到,Java内存模型几乎和计算机内存模型保持一致。
内存模型总述
Java中的缓存一致性问题: . 考虑下面这个代码: i = 10++; 假设现在有两个线程同时执行上述代码, 根据内存模型总述第1条,这两个线程都是从主内存中拷贝i到自己的工作内存当中,然后分别对其执行自加,然后将其放回主内存。 此时,尽管“自加了两次”,但实际上i=11,而非12。 这种被多个线程访问的变量为共享变量。
通常情况下,当一个cpu需要读取主存的时候它会将主存的部分读取到cpu缓存中。它甚至会将缓存的部分内容读到内部寄存器里,然后在寄存器中执行操作,当cpu需要将结果回写到主存的时候,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。
Heap就是主内存,Thread Stack就是工作内存
通过图可以看出java内存模型与硬件架构之间存在一些差异,硬件内存架构它没有区分线程栈和堆,对于硬件而言所有的线程栈和堆都分布在主内存里,部分cpu栈和堆可能出现cpu缓存中和cpu内部的寄存器里面。
一个操作是不可中断的,即便是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
对基本数据类型的变量读取和赋值的操作都是原子操作,即这些操作是不可中断,要么执行,要么不执行。
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
以上语句只有语句1是原子性操作。
Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
也就是说:在并发时,程序的执行可能会出现乱序。
另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
一个线程修改了某一个共享变量的值,其他线程是否能够立刻知道这个修改。(可见会牺牲性能)。
通过volatile关键字保证可见性:
普通的共享变量不能保证可见性,因为共享变量被修改后,什么时候被写入主存是不确定的,当其他线程去读取的时候,此时内存中可能还是原来的旧值,因此无法保证可见性。 通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
如果不满足以上规则,则虚拟机就会对其进行指令重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,重排序分为以下三种类型
1、编译器优化的重排序--编译器在不改变单线程程序语义的情况下,可以重新安排语句的执行顺序
2、指令级并行的重排序--如果不存在数据依赖,处理器可以改变语句对应机器执行的语句顺序
3、内存系统的重排序--由于处理器使用缓存和读/写缓存,这使得加载和存储操作看上去像是在乱序执行
在执行程序时,java内存模型确保在不同的编译器和不同的处理器平台上,来插入内存屏障来禁止特定的编译器重排序和处理器重排序,从而为上层提供内存一致性的条件。
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下八种操作来完成: