Java架构师学习之路之并发编程一: 缓存一致性协议和JMM
1. 现代计算机结构
现代计算机结构如上图,其中我们需要了解的部分是:
- CPU有高速缓存——CPU Cache
- CPU和内存的交互需要通过I/O总线
- CPU频率高于内存频率——也就是内存速度跟不上CPU的速度
- CPU有多核,可能还支持超线程
- I/O总线有带宽限制
2. CPU多级缓存架构
由上图我们可以看到,CPU是有3级高速缓存的。
各位可以打开任务管理器,选择性能标签页,左侧选中CPU,右侧则可以看到这3级缓存。
为什么要设计3级缓存:
- 内存频率低,CPU频率高,解决内存频率不足引起的性能瓶颈
- I/O总线有贷款限制,频繁和内存交互会导致I/O总线带宽不够,引起性能下降。
寄存器:
寄存器是离CPU最近的存储器,也是CPU内部内存的基础。
CPU读取存储器数据的流程:
- 读取寄存器数据,直接读取
- 读取L1缓存的数据,需要先lock缓存行(缓存行是3级缓存的最小存储单位,机械硬盘的最小存储单位为‘簇’),读取数据,最后unlock
- 读取L2缓存的数据, 需要先从L1取,如果没有,则去L2取,在L2中lock缓存行,复制数据到L1,再执行步骤2,最后unlock
- 读取L3缓存的数据同L2一样,先去L1和L2取,没有再从L3取
- 读取主内存数据最为复杂:通知内存控制器占用总线带宽,通知内存加锁,发起内存读请求,等待回应,回应数据保存到L3(如果没有就到L2),再从L3/2到L1,再从L1到CPU,之后解除总线锁定。
3. 上述架构在多线程环境下的问题
1). 缓存一致性问题
当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等。
2). 指令重排问题
为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化。
4. 线程
1). 什么是线程
线程是CPU调度的最小单位,也叫轻量级进程。
一个进程里可以拥有多个线程。
多个线程在CPU上高速切换,让用户感觉是在同时执行。
2). 线程的分类
线程分为两类:
A. 用户级线程 (ULT)
B. 内核级线程 (KLT)
什么是用户级线程(ULT):
指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。另外,用户线程是由应用进程利用线程库创建和管理,不依赖于操作系统核心。不需要用户态/核心态切换,速度快。操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。
我们在Java代码中new一个thread,这就是用户级线程。
什么是内核级线程(ULT):
线程的所有管理操作都是由操作系统内核完成的。内核保存线程的状态和上下
文信息,当一个线程执行了引起阻塞的系统调用时,内核可以调度该进程的其他线程执行。在
多处理器系统上,内核可以分派属于同一进程的多个线程在多个处理器上运行,提高进程执行
的并行度。由于需要内核完成线程的创建、调度和管理,所以和用户级线程相比这些操作要慢
得多,但是仍然比进程的创建和管理操作要快。大多数市场上的操作系统,如Windows,
Linux等都支持内核级线程。
注:我们在Java中无法直接创建内核线程!
那么Java程序中创建的用户级线程是怎样同CPU交互的呢?请看下图:
比如
new Thread().start();
此时会先创建用户级线程,然后由JVM调用本地的线程库(比如linux中是pThread库)创建内核线程并调度CPU。
用户线程和内核线程数量是1:1的。
如何在JVM创建线程:
- new Thread().start();
- 使用JNI将native线程attach到JVM中
3). Java线程的生命周期:
5. 并发
并发的优缺点
优点:
- 充分利用多核CPU的能力,提高系统性能。
缺点:
- 线程过多会导致CPU频繁切换上下文,保存线程中间态信息会消耗CPU资源,导致性能下降。
- 线程过多会增加临界风险,可能导致死锁。
- 其它与业务相关的问题。
6.JMM模型
1). 什么是JMM
- JMM全称为JAVA Memory Model,它是一种模型,一种规范。
- JMM围绕原子性、可见性、有序性展开。
2). JMM模型下线程和内存的交互
如下图:
3). JMM同步的八种操作
- lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
- unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的
变量才可以被其他线程锁定 - read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,
以便随后的load动作使用 - load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作
内存的变量副本中 - use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存
的变量 - store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,
以便随后的write的操作 - write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送
到主内存的变量中
4). 为什么要设计JMM
- 不同厂商生产的CPU不同,则CPU中对于缓存一致性协议的实现不同(有MESI、MSI等等),指令重排优化也不同,遵守JMM协议的虚拟机让我们无需关心底层硬件不同带来的对程序的影响。
- 不同操作系统使用的线程库不同,遵守JMM协议的虚拟机会自动帮助我们创建与用户线程对应的内核线程,而我们无需关心要使用哪个线程库去创建内核线程才能对CPU进行调度。
欢迎大家一起讨论,让我们一起向着架构师前进~~~
下一章:Java架构师学习之路之并发编程二: volatile关键字