# Preface

## 管程

**管程（Monitor）**&#x4F5C;为一种解决并发问题的模型，是继**信号量模型**之后的一项重大创新，它与信号量在逻辑上是等价的（可以用管程实现信号量，也可以用信号量实现管程），但是相比之下管程更易用。

**管程，指的是管理共享变量以及对共享变量的操作过程，让他们支持并发。**

管程有三种模型：**Hasen 模型**、**Hoare 模型**、**MESA 模型**。

### MESA 模型

应用最广泛的事 MESA 模型。

并发编程的两大核心问题：**互斥**、**同步**。

#### 管程解决互斥问题

**将共享变量及其对共享变量的操作统一封装起来**。可以看出，管程和面向对象的思路类似，这可能也是 Java 选择管程模型的原因之一吧。

![](https://3232244687-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LYZow-MmROshIrkwdtE%2F-Lc9NJcK51_abcnSb4-m%2F-Lc9O_hVLKOtS8WbmVWr%2Fimage.png?alt=media\&token=ed27b407-9bac-465f-ab41-443d9ecea62a)

#### 管程解决同步问题

* **入口等待队列**：当多个线程同时试图进入管程内部时，只允许一个线程进入，其他线程则在入口等待队列中等待。可参考 Java synchronized 实现中 [ObjectMonitor](https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/runtime/objectMonitor.hpp) 对象的 \_EntryList 字段。
* **条件变量**：
* **条件变量等待队列**：每个条件变量都对应有一个等待队列。可参考 Java synchronized 实现中 [ObjectMonitor](https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/runtime/objectMonitor.hpp) 对象的 \_WaitSet 字段。
  * **进入条件变量等待队列**：在 Java 里面采用 wait() 或 Condition.await() 实现。
  * **通知条件变量**：在 Java 里面采用 notify()/notifyAll() 或 Condition.signal() 实现。

![](https://3232244687-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LYZow-MmROshIrkwdtE%2F-Lc9NJcK51_abcnSb4-m%2F-Lc9OiS4qc0Zvn42sQ5P%2Fimage.png?alt=media\&token=189b5c43-d2cf-4fbb-9c36-2ebfa8afdae9)

### 三种模型对比

管程的三种模型的核心区别在于如何通知相关线程：

* **Hasen 模型**： 要求通知放在最后，线程 T2通知 T1后，T2就运行结束了，然后 T1执行。
* **Hoare 模型**：T2 通知T1后，T2阻塞，T1执行；等 T1执行完，唤醒 T2。所以 T2多了一次阻塞唤醒操作。
* **MESA 模型**：T2通知T1后，T2继续执行，T1从条件等待队列进入入口等待队列。
  * **优点**：通知不用放在最后，T2也没有多余的阻塞唤醒操作。
  * **缺点**：当 T1再次执行的时候，可能条件已经变化不满足了。所以 MESA 模型需要以循环的方式检验条件变量，有一个编程范式，见下面代码。

```java
while(条件不满足) {
  wait();
}
```

### `notify()` 何时用

一般来说，**除非经过深思熟虑，否则尽量使用 `notifyAll()`。**

当满足以下三个条件时，可以使用 notify()：

1. 所有等待线程拥有相同的等待条件；
2. 所有等待线程被唤醒后，执行相同的操作；
3. 只需要唤醒一个线程

### Java 与 管程

管程有三种模型，Java 参考了 MESA 模型，并实现了它。

`synchronized`、`wait()`、`notify()` 不过是操作系统领域里管程模型的一种实现而已。Java SDK 并发包里的 `Lock` 和 `Condition` 是管程的另一种实现。

* 内置的 `synchronized` 是对 MESA 模型的精简，MESA 模型可以有多个条件变量，synchronized 只有一个。但是使用简单，会自动加锁、解锁。
* JDK 并发包的 `Lock` 和 `Condition` 是另一套实现，支持多个条件变量，但是需要手动加锁和解锁。

## 信号量

**信号量（Semaphore）** 在1965年提出，之后一直都是并发编程领域的王者，直到管程模型被提出。

![信号量模型](https://3232244687-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LYZow-MmROshIrkwdtE%2F-LcJGG5pB9W4YJ3DFJBN%2F-LcJGfhIyobeKayelm5V%2Fimage.png?alt=media\&token=def86647-9730-45e4-9d88-168781d58b70)

信号量模型：一个计数器、一个等待队列、三个方法。

* **`init()`**：设置计数器初始值。
* **`down()`**：计数器减 1；如果此时计数器的值小于 0，则当前线程进入等待队列阻塞，否则当前线程可以继续执行。
* **`up()`**：计数器的值加 1；如果此时计数器的值小于或者等于 0，则唤醒等待队列中的一个线程，并将其从等待队列中移除。

这三个方法都是原子性的，由实现方保证。

Java 的信号量由`java.util.concurrent.Semaphore`实现，`up()`对应`acquire()`，`down()`对应`release()`。

## 分工、同步、互斥

**并发编程可以总结为三个核心问题：分工、同步、互斥。**

![](https://3232244687-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LYZow-MmROshIrkwdtE%2F-Lbx-lG8MWYpRtcqDggP%2F-Lbx65cet7xfnTGXI7CV%2Fimage.png?alt=media\&token=6354d09a-9bf0-4927-8900-be0ae43ddd32)

为了性能，如 IO 等待的时候不能让 CPU 闲着，所以我们把任务拆分交替执行，有了**分时操作系统**，出现了**并发**，后来 CPU 多核了又有了**并行**计算，也就是分工。

线程之间需要**通信**，于是操作系统提供了一些让进程，线程之间通信的方式，也就是**同步**。

并发和通信带来了多线程并发操作**共享资源**的问题，又要将对共享资源的访问串行化，所以设计了了锁，信号量等，也就是**互斥**。

### **分工**

**分工**指的是如何高效地拆解任务并分配给线&#x7A0B;**。**

Java SDK 并发包里的 Executor、Fork/Join、Future 本质上都是一种分工方法。

并发编程领域的设计模式：生产者 - 消费者、Thread-Per-Message、Worker Thread 模式等都是用来指导你如何分工的。

### 同步

**同步**指的是线程之间如何协作。

Java SDK 并发包里的 Executor、Fork/Join、Future 本质上都是分工方法，但同时也能解决线程协作的问题。Java SDK 里提供的 CountDownLatch、CyclicBarrier、Phaser、Exchanger 也都是用来解决线程协作问题的。

**协作问题的本质：**&#x5F53;某个条件不满足时，线程需要等待，当某个条件满足时，线程需要被唤醒执行。

解决协作问题的核心技术是**管程**，上面提到的所有线程协作技术底层都是利用管程解决的。

### **互斥**

**互斥**是保证同一时刻只允许一个线程访问共享资源。

分工、同步主要强调的是性能，但并发程序里还有一部分是关于正确性的，用专业术语叫“**线程安全**”。

不正确的主要源头是**可见性**问题、**有序性**问题和**原子性**问题。Java 引入内存模型可以避免可见性问题、有序性问题，但是还不足以完全解决线程安全问题。解决线程安全问题的核心方案还是**互斥**。

实现互斥的核心技术就是**锁**，Java 语言里 `synchronized`、SDK 里的各种 `Lock` 都能解决互斥问题。

CountDownLatch 就是一种典型的同步方式，而可重入锁则是一种互斥手段。

## 微观

CPU、内存、I/O 设备这三者的速度差别非常大，为了合理利用 CPU 的性能，平衡三者的速度，一般有如下措施：

* **硬件**：CPU 增加缓存，均衡 CPU 与内存的速度。会带来**可见性**问题。
* **操作系统**：增加进程、线程，分时复用 CPU，均衡 CPU 与 IO 设备的速度。会带来**原子性**问题。
* **编译器**：优化指令执行顺序，使缓存能更加合理利用。会带来**有序性**问题。

但是这三个优化又带来了问题。

### 可见性

CPU 缓存导致可见性问题。一个线程对共享变量的修改，另外一个线程能够立刻看到，我们称为**可见性**。

单核 CPU 倒是没什么问题，但是多核 CPU 每核有自己的缓存，会导致可见性问题。

### 原子性

分时操作系统的线程切换带来原子性问题。一个或者多个操作在 CPU 执行的过程中不被中断的特性称为**原子性**。CPU 能保证的原子操作是 **CPU 指令**级别的，而不是高级语言的操作符。

32位机器上对 long 进行操作不具有原子性。所以对 long 和 double 类型的变量，如有并发操作，最好加 volatile 或加锁。

### 有序性

编译优化带来有序性问题。

如下代码是双重检查机制创建单例对象，但是有问题。在 new 一个对象时，重排序后可能的执行方式为：

1. 分配一块内存 M；
2. 将 M 的地址赋值给 instance 变量；
3. 最后在内存 M 上初始化 Singleton 对象。

若线程 A 拿到锁，执行了 new 方法，然后切换到线程 B，那么有可能线程 B 判断 instance 不为 null（锁外面的检查），但是 instance 的内容还没初始化。可以用 **`volatile`** 解决，禁止指令重排序。

```java
public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}
```

## 宏观

并发编程中我们需要注意的问题有很多，主要是：**安全性问题**、**活跃性问题**、**性能问题**。

### 安全性问题

**线程安全**本质上就是正确性，而正确性的含义就是**程序按照我们期望的执行**。上文已经提到并发 Bug 的源头是原子性、可见性、有序性。但并不是所有的代码都需要分析上面三个问题，只有一种情况需要：**存在共享数据并且该数据会发生变化，通俗地讲就是有多个线程会同时读写同一数据**。

所以若能做到不共享数据或者数据状态不发生变化，也能够保证线程安全。这类技术方案有线程本地存储（**Thread Local Storage**，TLS）、**不变模式**等等。

当多个线程同时访问同一数据，并且至少有一个线程会写这个数据的时候，叫做**数据竞争**（**Data Race**）。

**竞态条件（Race Condition）**：**程序的执行结果依赖线程执行的顺序**。也可以这样理解**竞态条件，**&#x5728;并发场景中，程序的执行依赖于某个状态变量，当某个线程发现状态变量满足执行条件后，开始执行操作；可是就在这个线程执行操作的时候，其他线程同时修改了状态变量，导致状态变量不满足执行条件了。

面对数据竞争和竞态条件问题，都可以用**互斥**这个技术方案，逻辑上可以统一为：**锁**。

### 活跃性问题

**活跃性问题**：某个操作无法执行下去。有三种情况：

* **死锁**：一组互相竞争资源的线程因互相等待，导致“永久”阻塞的现象。
* **活锁**：线程虽然没有发生阻塞，但仍然会存在执行不下去。可以用一个**随机等待时间**解决。Raft 分布式一致性算法中也用到了。
* **饥饿**：线程因无法访问所需资源而无法执行下去。
  * 若线程优先级不均匀，低优先级线程执行机会很小，则可能发生饥饿；
  * 持有锁的线程，执行时间太长，也可能导致饥饿；
  * 解决方案：一保证资源充足，二公平分配资源，三避免持有锁的线程执行过长。一、三比较难满足，第二点可以用**公平锁**（先来后到的方案，线程的等待是有顺序的）来解决。

### 性能问题

锁的过度使用可能导致串行化的范围过大，这样就不能够发挥多线程的优势了。

* **无锁的算法和数据结构**：线程本地存储 (Thread Local Storage, TLS)、写入时复制 (Copy-on-write)、乐观锁等；原子类是无锁的数据结构；Disruptor 是一个无锁的内存队列。
* **减少锁持有的时间**：使用细粒度的锁（如 ConcurrentHashMap 1.8之前），读写锁等。

性能指标：

* **吞吐量**：单位时间内能处理的请求数量。
* **延迟**：从发出请求到收到响应的时间。
* **并发量**：能同时处理的请求数量。一般来说随着并发量的增加、延迟也会增加。所以延迟这个指标，一般都会是基于并发量来说的。例如并发量是 1000 的时候，延迟是 50 毫秒。
