JMM

# CPU

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

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

# CPU Cache

CPU Cache 模型图如下

各个 CPU 缓存与主内存访问对比图如下:

Cache 的出现就是为了解决 CPU 直接访问内存效率低下的问题。

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

# CPU 缓存一致性问题

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

# 1、通过总线加锁的方式

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

# 2、通过缓存一致性协议

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

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

# 并发编程的三个重要特性

# 原子性

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

# 有序性

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

# 可见性

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

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

# 原子性

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

# 有序性

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() 方法之前。

对象初始化才有被回收这一说。

# 可见性

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

# 使用关键字 volatile

被 volatile 修饰的共享变量。

读操作会直接在主内存进行。(工作内存已缓存共享资源,在被其他线程修改之后,会失效,仍然需要从主内存中获取)

写操作,先修改工作内存,修改结束后会立即刷新到主内存中。

# 使用关键字 synchronized

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

# 使用 JUC 提供的显示锁 LOCK

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

# volatile

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

# volatile 关键字的语义

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

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

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

# volatile 实现原理

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

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

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

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

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

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

# volatile 使用场景

# 开关控制

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

public class ThreadCloseale extends Thread{
  // 该线程是否关闭
  private volatile boolean close = false;
  @Override
  public void run(){
    while(!close){
      // do nothing
    }
  }

  public void shutdown(){
    this.close = true;
  }
}

当线程 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() 之前。

# 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 会使线程进入阻塞状态。
精彩内容推送,请关注公众号!
最近更新时间: 4/19/2020, 4:27:40 PM