目录
一、CPU缓存结构
二、并发编程的三大问题
原子性
可见性
有序性
三、JMM内存模型
四、JMM如何解决有序性问题
由于CPU的运算速度比主存(物理内存)的存取速度快很多,为了提高处理速度,现代CPU不直接和内存进行通信,而是在CPU和主存之间设计了高速缓存(Cache),越靠近CPU层的高速缓存速度越快,容量越小。如下图
每一级高速缓存中所存储的数据都是下一级高速缓存中的一部分,L1最靠近CPU所以读取速度最快。L1和L2高速缓存只能被一个单独的CPU内核使用,L3高速缓存可以被同一个CPU芯片上的所有CPU内核共享,而主内存可以由系统中所有的CPU共享。CPU读取数据时,首先从L1层高速缓存中读取数据,如果没有读取到,再到L2,L3高速缓存中读取数据,如果都没有读取到数据,就会去主存中读取数据。
原子性就是指不可中断的一个或者一些列操作,不能被线程调度机制打断的操作。如下面代码
public class Test01 {
private int count=0;
public void increase(){
System.out.println(count++);
}
}
将该java文件通过一下指令编译成class文件。javac -encoding UTF-8 Test01.java。通过javap反编译。
可知++操作并不是原子性的,而是进行了取值,运算,赋值,返回四个操作,在多线程并发的情况下,会发生原子性的问题,所以不是线程安全的。
一个线程对共享变量的修改,另一个线程能够立即可见,那么这个变量具有内存可见性。
JAVA内存模型
Java内存模型只是一种规范,抽象的概念,不是具体存在的。注意和JVM内存结构不同。
java内存模型规定
1、所有的变量存储在主内存中
2、每一个线程都有自己的工作内存,且对变量的操作都是在自己的工作内存中进行的。
3、不同的线程之间无法直接访问彼此工作内存中的变量,要想访问只能通过主存来传递(线程通信)
在java中,所有的局部变量,方法定义的参数都不会在线程之间共享,所以也就没有可见性的问题。而定义在堆中的共享变量,类,数组元素等都是线程共享的,存在可见性的问题。解决可见性问题可以使用volatile关键字解决。
有序性是指程序执行的顺序按照代码的先后顺序执行。但是在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序,它不保证程序各个语句的执行顺序严格按照代码顺序执行,但是它会保证程序最终的结果和顺序执行结果一致。但是这只是针对单线程运行,如果在多线程条件下,重排序会出现内存可见性问题,导致线程不安全。
重排序分三种类型
1、编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2、指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3、内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
JAVA中synchronized、volatile可以解决重排序。
JMM规定将所有的变量(不包括局部变量)都存放在公共内存中,当线程使用变量时会把主存的变量复制到自己的工作空间(私有内存),线程对变量的读写操作,都是在自己的工作空间内完成的。操作完成之后,再把自己私有内存的变量刷新到主内存中。但是如果两个或者多个线程同时操作同一个变量就会发生可见性问题。
JMM定义了一套自己的主存与工作内存之间的交互协议,即变量如何从主存到工作内存,又如何从工作内存写入到主内存。该协议有8种操作,并且要求JVM具体实现必须保证其中每一种操作都是原子性的。
操作 | 作用对象 | 说明 |
---|---|---|
Read(读取) | 主存 | Read操作把一个变量的值从主存传输到工作内存,以便Load操作使用。 |
Load(载入) | 工作内存 | Load操作把Read操作主存中的变量值载入工作内存的变量副本中。 |
Use(使用) | 工作内存 | Use操作把工作内存中的一个变量值传递给执行引擎。每当JVM遇到一个需要使用变量值的字节码指令时,执行Use操作。 |
Assign(赋值) | 工作内存 | 执行引擎通过Assign操作给工作内存中的变量赋值。每当JVM遇到一个给变量赋值的字节码指令时执行Assign操作。 |
Store(存储) | 工作内存 | Store操作把工作内存中的变量的值传递到主存中,以便随后的Write操作。 |
Write(写入) | 主存 | Write操作把Store操作从工作内存中得到的变量值写入到主存变量中。 |
Lock(锁定) | 主存 | 把一个变量标识为一个线程独占的状态 |
Unlock(解锁) | 主存 | 把一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程使用。 |
这八种操作必须满足以下规则
JMM提供了自己的内存屏障指令,要求JVM编译器实现这些指令,禁止特定类型的编译器和CPU重排序(不是所有的编译器重排序都要禁用)。
JMM内存屏障主要有Load和Store两类
Load Barrier(读屏障):在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主存中加载数据。
Store Barrier(写屏障):在写指令之后插入写屏障,能让写入缓存的最新数据写回主存。
但是在实际使用时,会对Load Barrier和Store Barrier 组合使用
参考:
《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著
Java 并发 - 理论基础 | Java 全栈知识体系