# 计算机硬件的效率与一致性

  • 现代计算机通过加入读写速度尽可能接近处理器运算器运算速度的高速缓存(Cache)用于运算过程。

缓存一致性

  • 共享内存多核系统中多个处理器的运算任务涉及到同一块主内存区域,将可能导致各自的缓存数据不一致。
  • 缓存一致性协议:要求处理器在访问缓存时遵循协议(MSI、MESI、MOSI、Synapse、 Firefly 及 Dragon Protocol 等)。
  • 处理器缓存以及主内存之间交互关系
  • 乱序执行优化:为使处理器运算单元尽量被充分利用,处理器对输入代码乱序执行优化,并对执行结果重组保证结果与顺序执行结果一致。

# Java 内存模型

  • Java 使用自定义的内存模型,来屏蔽各种硬件和操作系统的内存访问差异,以实现跨平台中一致的内存访问效果。
  • 定义程序中各种(存在线程竞争关系的)变量的访问规则。

# 主内存与工作内存

  • 所有变量都存储在主内存中。
  • 每条线程有自己的工作内存。
  • 线程工作内存中保存被该线程使用的变量的主内存副本。
  • 线程对变量的所有操作都必须在工作内存中进行。
  • 工作内存为线程私有。
  • 线程间变量值传递均需要通过主内存完成。
  • 工作内存可能会优先存储与寄存器和高速缓存中。
  • 三者间关系:

# 内存间交互操作

Java 内存模型定义的 8 种原子操作

  • lock(锁定):作用于主内存的变量, 它把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量, 它把一个处于锁定状态的变量释放出来, 释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量, 它把一个变量的值从主内存传输到线程的工作内存中, 以便随后的 load 动作使用。
  • load(载入):作用于工作内存的变量, 它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量, 它把工作内存中一个变量的值传递给执行引擎, 每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量, 它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量, 它把工作内存中一个变量的值传送到主内存中, 以便随后的 write 操作使用。
  • write(写入):作用于主内存的变量, 它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
  • 规定顺序执行的操作(不要求连续):
  • read 和 load:把一个变量从主内存拷贝到工作内存。
  • store 和 write:把变量从工作内存同步回主内存。
对8种内存访问操作的其他约束
  • 不允许 read 和 load、 store 和 write 操作之一单独出现:不允许一个变量从主内存读取了但工作内存不接受, 或者工作内存发起回写了但主内存不接受的情况出现。
  • 不允许一个线程丢弃它最近的 assign 操作:变量在工作内存中改变了之后必须把该变化同步回主内存。
  • 不允许一个线程无原因地(没有发生过任何 assign 操作) 把数据从线程的工作内存同步回主内存中。
  • 一个新的变量只能在主内存中 “诞生”:对一个变量实施 use、 store 操作之前, 必须先执行 assign 和 load 操作。
  • 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次, 多次执行 lock 后, 只有执行相同次数的 unlock 操作, 变量才会被解锁。
  • 如果对一个变量执行 lock 操作, 那将会清空工作内存中此变量的值, 在执行引擎使用这个变量前, 需要重新执行 load 或 assign 操作以初始化变量的值。
  • 如果一个变量事先没有被 lock 操作锁定, 那就不允许对它执行 unlock 操作, 也不允许去 unlock 一个被其他线程锁定的变量。
  • 对一个变量执行 unlock 操作之前, 必须先把此变量同步回主内存中(执行 store、 write 操作)。

# 对于 volatile 型变量的特殊规则

volatile 修饰的变量具备有两项特性:

  • 保证此变量对所有线程的可见性:当一条线程修改了这个变量的值, 新值对于其他线程来说是可以立即得知的。 (volatile 变量在并发环境下仍存在安全问题)
  • 禁止指令重排序优化。

使用 volatile 变量仍需加锁的情况

  • 运算结果并不依赖变量的当前值, 或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。

volatile 变量的定义:

  • 每次使用前都必须先从主内存获取最新的值。
  • 每次修改后都必须立刻同步回主内存。
  • volatile 变量不会被指令重排序优化,保证代码执行顺序与程序的顺序相同。
双锁检测(DCL)单例模式代码
public class Singleton {
  private volatile static Singleton instance;
  public static Singleton getInstance() {
    if (instance == null) {
      synchronized (Singleton.class) {
        if (instance == null) {
          instance = new Singleton();
        }
      }
    }
    return instance;
  } 
  public static void main(String[] args) {
    Singleton.getInstance();
  }
}
  • 对应的反汇编代码
0x01a3de0f: mov $0x3375cdb0,%esi      ;...beb0cd75 33
                                      ; {oop('Singleton')}
0x01a3de14: mov %eax,0x150(%esi)      ;...89865001 0000
0x01a3de1a: shr $0x9,%esi             ;...c1ee09
0x01a3de1d: movb $0x0,0x1104800(%esi) ;...c6860048 100100
0x01a3de24: lock addl $0x0,(%esp)     ;...f0830424 00
                                      ;*putstatic instance
                                      ; - Singleton::getInstance@24
  • 内存屏障:有 volatile 修饰的变量,在赋值后(mov % eax,0x150 (% esi))多执行 “lock addl $0x0,(% esp)”(把 ESP 寄存器的值加 0 的空操作)的操作。

指令重排序

  • 处理器采用允许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元进行处理。
  • 必须能正确处理指令依赖情况保障程序能得出正确的执行结果。
  • 重排序时不能把后面的指令重排序到内存屏障之前的位置。
  • lock 指令作用:将本处理器的缓存写入了内存, 该写入动作也会引起别的处理器或者别的内核无效化。相当于(“store 和 write” 操作)。

# 64 位数据类型特殊规则

long 和 double 的非原子性协定

  • 内存模型允许虚拟机将 volatile 修饰的 64 位数据的读写操作划分为两次 32 位的操作来进行。
  • 现代 CPU 包含专用于处理浮点数据(double)的浮点运算器,因此通常不会出现非原子性访问的问题。

# 原子性、可见性与有序性

原子性

  • 基本数据类型的访问、读写都是具备原子性的。
    可见性
  • 当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。
  • volatile 变量可见性。
  • synchronized 同步块:对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存。
  • final 修饰的字段。
    有序性
  • 如果在本线程内观察,所有的操作都是有序的。
  • 如果在一个线程中观察另一个线程,所有的操作都是无序的。

# 先行发生原则

内存模型定义的 “先行发生”

  • 程序次序规则:一个线程内按照控制流顺序,书写在前面的操作先行发生与书写在后面的操作。
  • 管程锁定规则:同一个锁,unlock 操作先行发生于后面对同一个锁的 lock 操作。
  • volatile 变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
  • 线程启动规则:Thread 对下个的 start () 方法先行发生于此线程的每个动作。
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。
  • 线程中断规则:对线程 interrupt () 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  • 对象终结规则:一个对象的初始化完成先行发生于他的 finalize () 方法的开始。
  • 传递性:先行发生关系的传递性。

# Java 与线程

# 线程实现

实现线程方式:

  • 内核线程实现(1:1)
  • 由操作系统内核完成线程切换。
  • 轻量级进程(LWP)与内核线程(KLT)之间时 1:1 的关系
  • 内核线程
  • 用户线程实现(1:N)
  • 用户线程的建立、同步、销毁和调度完全在用户态中完成,操作快速、低消耗。
  • 需考虑到线程创建、销毁、切换和调度程序实现复杂。
  • 操作系统只把处理器资源分配到进程。
  • 进程与用户线程之间一对多的线程模型(1:N)。
  • 用户线程
  • 用户线程加轻量级进程混合实现(N:M)
  • 用户线程:完全建立在用户空间,线程的创建、切换、析构等操作依然廉价,并支持大规模用户线程并发。
  • 轻量级进程:可以使用内核线程的调度功能及处理器映射,并降低了整个进程被完全阻塞的风险(多对多的线程模型)。
  • 混合线程
  • 主流平台上的 Java 虚拟机中 HotSpot 虚拟机使用 1:1(每一个 java 线程直接映射到一个操作系统原生线程)。

# Java 线程调度

线程调度:系统为线程分配处理器使用权的过程。

  • 协同式线程调度
  • 线程自己控制执行时间。
  • 不存在线程同步问题。
  • 缺点:线程执行时间不可控制,容易发生程序阻塞。
  • 抢占式线程调度
  • 由系统分配执行时间。
  • Java 所使用的调度方式:
  • Java 使用线程优先级(10 个级别:Thread.MIN_PRIORITY 至 Thread.MAX_PRIORITY)引导操作系统来分别分配执行时间。
  • 线程优先级不能完全准确判断出线程的执行先后:
  • java 中线程优先级和操作系统中优先级并非一一对应。
  • 操作系统(Windows)使用优先级推进器,可能会越过线程优先级为线程分配执行时间。

# 线程状态转换

任意时间点一个线程只能有其中一种状态:

  • 新建(New)
  • 运行(Runnable)
  • 包括操作系统线程状态中 Running 和 Ready
  • 无限期等待(Waiting)
  • 需要被其他线程显式唤醒。
  • 没有设置 Timeout 参数的 Object::wait () 方法;
  • 没有设置 Timeout 参数的 Thread::join () 方法;
  • LockSupport::park () 方法。
  • 限期等待(Timed Waiting)
  • 一定时间后由系统自动唤醒。
  • Thread::sleep () 方法;
  • 设置了 Timeout 参数的 Object::wait () 方法;
  • 设置了 Timeout 参数的 Thread::join () 方法;
  • LockSupport::parkNanos () 方法;
  • LockSupport::parkUntil () 方法;
  • 阻塞(Blocked)
  • 结束(erminated)
  • 线程状态转换关系:
    线程状态转换

# Java 与协程

内核线程的局限

  • web 应用随着请求数量和业务细分后复的服务数量不断增加。
  • java 主流的 1:1 的内核线程模型在每个请求执行时间变短、数量变多的前提下,用户线程切换开销会造成严重浪费。
  • 传统 Java Web 服务器线程池容量在几十个到两百之间。
    协程:使用用户线程来优化原本使用内核线程的切换开销:保护和恢复现场的成本。
  • 负责协同式调度线程保护、恢复的用户线程。
  • 有栈协程:完整地做调用栈的保护、恢复工作。

# Java 中协程解决方案

  • 纤程:有栈协程的一种特例实现。

新并发编程模型:

  • 执行过程:维护执行现场,保护、恢复上下文状态。
  • 调度器:编排所有要执行的代码顺序。