1. 硬件内存模型
按照数据读取顺序和 CPU 的紧密程度,CPU 的缓存可以分为一级缓存(L1)、二级缓存(L2)、三级缓存(L3),每一级缓存存储的数据都是下一级的一部分。
1.1. 数据加载
1.1.1. 处理流程
- 依次从一级缓存、二级缓存、三级缓存中查找,如果没找到再从主内存中查找。
- 把找到的数据依次加载到多级缓存中,下次再使用相关的数据直接从缓存中查找。
1.1.2. 缓存行
加载内存中连续的数据,一般来说是加载连续的 64 个字节,因此,如果访问一个 long
类型(4个字节)的数组时,当数组中的一个值被加载到缓存中时,另外 7 个元素也会被加载到缓存中,这就是 “缓存行” 的概念。
1.2. 执行流程
为了充分利用 CPU 中的运算单元,CPU 可能会对输入的代码进行乱序执行优化,然后在计算之后再将乱序执行的结果进行重组,保证该结果与顺序执行的结果一致,但并不保证程序中各个语句计算的先后顺序与代码的输入顺序一致,因此,如果一个计算任务依赖于另一个计算任务的结果,那么其顺序性并不能靠代码的先后顺序来保证。
2. Java 内存模型
Java 内存模型(Java Memory Model,JMM)是在硬件内存模型基础上更高层的抽象,它屏蔽了各种硬件和操作系统对内存访问的差异性,从而实现让 Java 程序在各种平台下都能达到一致的并发效果。目的是为了解决多线程环境下共享变量的一致性。
2.1. 内存划分
2.1.1. 内存模型
Java 内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存(类比 CPU 的高速缓存)。
- 工作内存中保存着该线程使用到的变量的主内存副本的拷贝。
- 线程对变量的操作都必须在工作内存中进行,包括读取和赋值等,而不能直接读写主内存中的变量。
- 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递必须通过主内存来完成。
2.1.2. 模型类比
- 主内存:主要对应于硬件内存,堆中对象的实例部分
- 工作内存:主要对应于 CPU 的高速缓存和寄存器部分,虚拟机栈中的部分区域
2.2. 主内存与工作内存之间的交互操作
2.2.1. 个交互协议
- lock,锁定,作用于主内存的变量,它把主内存中的变量标识为一条线程独占状态;
- unlock,解锁,作用于主内存的变量,它把锁定的变量释放出来,释放出来的变量才可以被其它线程锁定;
- read,读取,作用于主内存的变量,它把一个变量从主内存传输到工作内存中,以便后续的 load 操作使用;
- load,载入,作用于工作内存的变量,它把 read 操作从主内存得到的变量放入工作内存的变量副本中;
- use,使用,作用于工作内存的变量,它把工作内存中的一个变量传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
- assign,赋值,作用于工作内存的变量,它把一个从执行引擎接收到的变量赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时使用这个操作;
- store,存储,作用于工作内存的变量,它把工作内存中一个变量的值传递到主内存中,以便后续的 write 操作使用;
- write,写入,作用于主内存的变量,它把 store 操作从工作内存得到的变量的值放入到主内存的变量中;
操作要按顺序,但不一定要连续
如果要把一个变量从主内存复制到工作内存,那就要按顺序地执行 read 和 load 操作。
如果要把一个变量从工作内存同步回主内存,就要按顺序地执行 store 和 write 操作。
比如,对主内存中的变量 a 和 b 的访问,可以按照以下顺序执行:
read a -> load a -> read b -> load b,也可以是read a -> read b -> load b -> load a
2.2.2. 个基本规则
- 不允许 read 和 load、store 和 write 操作之一单独出现,即不允许出现从主内存读取了而工作内存不接受,或者从工作内存回写了但主内存不接受的情况出现;
- 不允许一个线程丢弃它最近的 assign 操作,即变量在工作内存变化了必须把该变化同步回主内存;
- 不允许一个线程无原因地(即未发生过 assign 操作)把一个变量从工作内存同步回主内存;
- 一个新的变量必须在主内存中诞生,不允许工作内存中直接使用一个未被初始化(load 或 assign)过的变量,换句话说就是对一个变量的 use 和 store 操作之前必须执行过 load 和 assign 操作;
- 一个变量同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一个线程执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才能被解锁。
- 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值;
- 如果一个变量没有被 lock 操作锁定,则不允许对其执行 unlock 操作,也不允许 unlock 一个其它线程锁定的变量;
- 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中,即执行 store 和 write 操作;
2.3. 一致性
一致性主要包含三大特性:原子性、可见性、有序性
2.3.1. 原子性
原子性是指一段操作一旦开始就会一直运行到底,中间不会被其它线程打断,这段操作可以是单个或多个操作。
⭐️ 保证指令不会受到上下文切换的影响。
比如对应一个静态全局变量 int i,两个线程同时对它赋值,线程 A 给他赋值 1,线程 B 给他赋值 - 1。那么 i 的值只能是 1 或者 - 1。线程 A 和线程 B 之间是没有干扰的。这就是原子性的特点,不可被中断。
- 单纯的赋值操作就是原子操作:
a = 1
- 复合的赋值操作不是原子操作:
a++
public class ThreadTest {
private static int count = 0;
public static void main(String[] args) {
// 线程1对count自增5000次
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) count++;
});
// 线程2对count自减5000次
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) count--;
});
thread1.start();
thread2.start();
}
}
- 理想情况下,两个线程运行结束后
count == 0
。 - 实际情况下,两个线程运行结束后
count != 0
。
i++
和 i--
在 java 中