TODO JMM

一、CPU

CPU 是完成计算机运算操作的机器,CPU 执行运算操作过程中需要涉及到相关数据的读写操作,而这些数据只能从计算机主存中获取。

不过 CPU 的执行速度远远快于从主存中获取数据,为了提升处理效率,于是 CPU 引入 Cache。

1.1、CPU Cache

CPU Cache 模型图如下
01.png
各个 CPU 缓存与主内存访问对比图如下:
02.png
Cache 的出现就是为了解决 CPU 直接访问内存效率低下的问题。

CPU 在执行计算的时候将所有需要操作的数据从主存中复制一份到 CPU Cache 中,之后所有的操作数据直接从 CPU Chache 中读取,当运算结束后,再将结果刷到主存中,以此来提高CPU的吞吐能力,CPU 与 准村之间的交换架构大致如下:
03.png

1.2、CPU 缓存一致性问题

只要引入缓存,就会有缓存一致性的问题,而解决缓存一致性问题。针对 CPU 缓存一致性问题,通两种主流的解决方案:

1.2.1、通过总线加锁的方式

该方式会阻塞其他 CPU 对其他组件的访问,从而使得只有抢到总先锁的一个CPU能够访问该变量内存,效率低下。

1.2.2、通过缓存一致性协议

04.png
缓存一致性协议中最出名的是 Intel 的 MESI 协议,MESI 通过如下操作保证共享变量的一致性问题:

  • 读操作:不做任何处理,只是将 Cache 中的数据读取到寄存器中
  • 写操作,发出信号通知其他 CPU 将变量的 Cache line 置为无状态,其他 CPU 在进行该变量读取的时候就需要到主存中在此获取。

二、并发编程的三个重要特性

原子性

在一次操作或多次操作中,要么所有的操作全部得到执行并执行完成,要么所有的操作都不执行。

有序性

程序代码在执行过程中的先后顺序,一般来说,处理器为了提高程序的运行效率,可能会对输入的代码指令做一定的优化,所以程序的编写顺序不等于执行顺序-指令重排序(排序的前提是不影响程序的正常运行)。

可见性

三、JMM 如何保证并发的三个特性

JMM 的设计类似于 CPU Cache 设计,JMM 规定了所有变量都是存储在主内存(RAM)中的,每个线程都有自己的工作内存或者本地内存,线程对变量的所有操作都必须在自己的工作内存中进行,而不能直接对主内存进行操作,并且每个线程都不能访问其他线程的工作内存或者本地内存。

3.1、原子性

JMM 保证了基本读取和赋值的原子性操作,其他的均不保证,如果想要使得某些代码片段具备原子性,需要使用 synchronized 或者 JUC 中的 lock 。如果想要使得 int 等类型自增操作具备原子性,可以使用 JUC 下的原子封装类型 java.util.concurrent.atomic.*

3.2、有序性

java 提供了三种方式来保证可见性

使用关键字 volatile

使用 synchronized 关键字

使用显示锁 Lock

除此之外符合 happens-before 原则的都具备有序性,具体原则如下

程序次序规则

在一个线程内,代码按照编写时的次序执行,编写在后面的操作发生于编写在前面的操作之后。 不过虚拟机可能会对程序代码指令进行重排序,只要确保在一个线程内最终的结果和代码顺序执行的结果一致即可。

锁定规则

一个 unlock 操作要先行发生于对同一个锁的 lock 操作。 无论多线程还是单线程,如果同一个锁是锁定状态,那么 unlock 操作一定在 lock 操作之前。

volatile 变量规则

对一个变量的写操作要早于对这个变量之后的读操作 一个变量被 volatile 修饰后,线程 A对其进行读操作,线程 B 对其进行写操作,那么写操作肯定发生在读操作之前。

传递规则

操作 A 先于操作 B,操作B先于操作C,那么操作 A 肯定先于操作 C。

线程启动规则

Thread 对象的 start() 方法先行发生于对该线程的任何动作 只有 start 之后, 线程才开始真正执行,否则 Thread 也只是一个对象而已

线程中断规则

对线程执行 interrupt() 方法肯定要优先于捕获到中断信息。 调用 interrupt() 方法之后,线程才会收到中断信息。

线程的终结规则

线程中所有的操作都要先行发生于线程的中止检测。 线程死亡了,逻辑单元便不再执行

对象的终结规则

一个对象初始的完成先行于 finalize() 方法之前。 对象初始化才有被回收这一说。

3.3、可见性

java 提供了三种方式来保证可见性

使用关键字 volatile

被 volatile 修饰的共享变量。 读操作会直接在主内存进行。(工作内存已缓存共享资源,在被其他线程修改之后,会失效,仍然需要从主内存中获取) 写操作,先修改工作内存,修改结束后会立即刷新到主内存中。

使用关键字 synchronized

synchronized 关键字能够保证同一时刻只有一个线程获取到锁,然后执行同步方法,并且确保锁释放前,会将对变量的修改刷新到主内存中。

使用 JUC 提供的显示锁 LOCK

Lock 能够保证只有一个线程获取到锁然后执行同步方法,且确保在锁释放前,会将对变量的修改刷新到主内存中。

四、volatile

volatile 只能修饰类变量和实例变量,对于方法参数、局部变量以及实例常量,类常量都不能进行修饰

4.1、volatile 关键字的语义

volatile 关键字不具备保证原子性的语义。

volatile 关键字具有保证顺序性的语义,也就是禁止对指令进行重排序操作。

volatile 关键字具备保证可见性的语义,多个线程对同一个共享变量进行操作时,线程 A 对变量X的修改操作,线程B能够马上知道变量 X被修改了。

4.2、volatile 实现原理

05.png
通过 OpenJDK 下的 unsafe.cpp 能够发现被 volatile 修饰的变量存在于一个 “lock;” 的前缀之下,而 “lock;” 前缀实际上是一个内存屏障,该内存屏障会为指令的执行提供一下几个保障

确保指令重排序时不会将其后面的代码排到内存屏障之前

确保指令重排序时不会将其前面的代码排到内存屏障之后

确保在执行到内存屏障修饰的指令时前面的代码全部执行完成

强制将线程工作内存中的值的修改刷新到主内存中

如果是写操作,则会导致其他线程工作内存(CPU Cache)中的缓存数据失效。

4.3、volatile 使用场景

开关控制

利用 volatile 的可见性特点进行开关控制,例如:

  1. public class ThreadCloseale extends Thread{
  2. // 该线程是否关闭
  3. private volatile boolean close = false;
  4. @Override
  5. public void run(){
  6. while(!close){
  7. // do nothing
  8. }
  9. }
  10. public void shutdown(){
  11. this.close = true;
  12. }
  13. }

当线程 A 在执行时,线程 B 调用的 shutdown() 方法使得 close = true,同时由于 volatile 的原因导致线程 A 的工作内存失效,重新存主内存中获取到 close =true 的值,此时线程 A 中止。

状态标记

利用 volatile 的顺序性特点,例如:

public class InitClass{
  // 该类是否初始化完成 true:完成,false:未完成
  private volatile boolean initialized = false;
  // 准备被初始化的类
  private Context context;
  public Context load(){
    if(!initialized){
      // 进行类的初始化操作
      context = loadContext();
      // 标记类已经初始化
      initialized = true;
    }
    return context;
  }
}

initialized 被 volatile 修饰了,volatile 可以防止重排序,也就是说, initialized = true 的操作,一定是在类初始的方法 loadContext() 之前。

4.4、volatile 对比 synchronized

使用上的区别

  • volatile 只能用于修饰实例变量或者类变量,不能用于修饰方法,方法参数,局部变量,常量等。
  • synchronized 不能用于变量的修饰,只能用于修饰方法或者语句块
  • volatile 修饰的变量可以为 null,shychronized 同步语句块的 monitor 对象不能为 null。

对原子性的保证

  • volatile 无法保证原子性
  • synchronized 是一种排他的机制,因此能够保证代码的原子性。

对可见性的保证

两者均可以保证共享资源在多线程间的可见性,但是实现机制不同。

  • volatile 使用机器指令 “lock;” 的方式迫使其他线程工作内存中的数据失效,不得不到主内存中在此加载。
  • synchronized 借助 JVM 指令 monitor 和 monitor exit 通过排他的方式使得同步代码串行化,在 monitor exit 时将所有共享资源的修改都刷新到主内存中。

对有序性的保证

两者均可以保证有序性,不过实现机制不同。

  • volatile 禁止 JVM 编译器以及处理器对其进行重排序
  • synchronized 通过排他禁止使得代码执行串行化。【虽然代码块中的指令会发生重排序,但是不影响最终结果】

其他

  • volatile 不会使线程陷入阻塞
  • synchronized 会使线程进入阻塞状态。