# Preface

## 管程

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

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

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

### MESA 模型

应用最广泛的事 MESA 模型。

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

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

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

![](/files/-Lc9O_hVLKOtS8WbmVWr)

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

* **入口等待队列**：当多个线程同时试图进入管程内部时，只允许一个线程进入，其他线程则在入口等待队列中等待。可参考 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() 实现。

![](/files/-Lc9OiS4qc0Zvn42sQ5P)

### 三种模型对比

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

* **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年提出，之后一直都是并发编程领域的王者，直到管程模型被提出。

![信号量模型](/files/-LcJGfhIyobeKayelm5V)

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

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

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

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

## 分工、同步、互斥

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

![](/files/-Lbx65cet7xfnTGXI7CV)

为了性能，如 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 毫秒。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yunzhao.gitbook.io/notes/java/concurrency/preface.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
