Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

后端工程 / Backend Engineering

1. Backend 后端技术全景学习路线 / A Complete Backend Engineering Learning Path

2. 分布式系统——为什么单体时代过去了 / Distributed Systems Beyond the Monolith

3. 并发编程——为什么你的库存总是扣成负数 / Concurrency Control and Inventory Consistency

4. MySQL 优化——为什么你的查询总是那么慢 / MySQL Query and Storage Optimization

5. 架构模式——什么时候该用 CQRS / Architecture Patterns and When to Use CQRS

6. 系统设计——如何设计一个每秒 10 万订单的秒杀系统 / Designing a High-Throughput Flash-Sale System

7. 数据库工程化——在线表结构变更怎么不停服 / Database Engineering and Online Schema Changes

8. 可观测性——出了问题怎么快速定位 / Observability and Rapid Production Diagnosis

本页目录

并发编程——为什么你的库存总是扣成负数 / Concurrency Control and Inventory Consistency ​

📅 创建时间:2026-05-08 🏷️ 标签:#线程池 #并发 #死锁 #CAS #协程 #竞态条件 📚 前置知识:[[00-backend-overview]] [[06-network]](连接与线程) 📚 相关知识:[[01-redis-deep]](Redis 分布式锁) [[09-mysql-optimization]](乐观锁)


场景:1000 人抢 10 部手机,库存变成了 -990 ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  双十一,1000 人同时抢购 10 部手机。                      │
│                                                             │
│  查询库存:10 部                                          │
│  扣减库存:10 - 1 = 9                                    │
│  1000 人并发 → 每个都查到 10 → 每个都扣 1                │
│  结果:库存 = 10 - 1000 = -990                            │
│                                                             │
│  10 部手机,卖了 1000 部。                               │
│                                                             │
│  你被叫去赔钱。                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这一章,我们理解并发编程的三大问题:竞态条件、死锁、线程池调参。


第1节:竞态条件——并发编程的第一杀手 ​

问题:为什么并发下数字会错 ​

python
import threading

# 库存(共享变量)
stock = 10

def buy():
    global stock
    # 读取 → 检查 → 写入(不是原子操作!)
    if stock > 0:
        # 这个间隙内,其他线程可能也在检查
        stock -= 1  # 写入
        print(f"购票成功,剩余 {stock}")

# 1000 个线程同时抢
threads = [threading.Thread(target=buy) for _ in range(1000)]
for t in threads: t.start()
for t in threads: t.join()

print(f"最终库存:{stock}")  # 大概率不是 0!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

推演:问题出在哪里 ​

┌─────────────────────────────────────────────────────────────┐
│                 竞态条件图解                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  线程 A                         线程 B                    │
│    │                              │                        │
│    │ ── read stock=10 ────▶     │                        │
│    │ ── check stock>0 ── YES ──▶│                        │
│    │                              │                        │
│    │                              │ ── read stock=10 ──▶  │
│    │                              │ ── check stock>0 ──▶  │
│    │                              │                        │
│    │ ── write stock=9 ───────▶  │                        │
│    │                              │                        │
│    │                              │ ── write stock=9 ──▶  │
│    │                              │                        │
│    │  问题:两个线程都买到了,但库存只扣了 1!           │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

解法一:互斥锁(最简单) ​

python
import threading

stock = 10
lock = threading.Lock()

def buy():
    global stock
    with lock:  # 互斥锁:同时只有一个线程能进入
        if stock > 0:
            stock -= 1
            print(f"购票成功")

threads = [threading.Thread(target=buy) for _ in range(1000)]
for t in threads: t.start()
for t in threads: t.join()

print(f"最终库存:{stock}")  # 一定是 0 或负数(取决于检查位置)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

解法二:CAS(Compare-And-Swap,无锁并发) ​

python
import threading
from concurrent.futures import ThreadPoolExecutor

# Python 的 CAS:atomic 操作
stock = 10

def buy():
    global stock
    while True:
        current = stock
        if current <= 0:
            break
        # CAS:如果 stock == current,就写成 current - 1
        if threading.local():
            pass  # Python GIL 限制,真正的 CAS 需要 atomic

# Java 的 CAS(无锁,性能更好):
# public class StockService {
#     private AtomicInteger stock = new AtomicInteger(10);
#
#     public boolean buy() {
#         // compareAndSet:原子操作
#         return stock.compareAndSet(current, current - 1);
#     }
# }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

第2节:死锁——两个线程互相等待对方 ​

场景:转账场景 ​

python
import threading

class Account:
    def __init__(self, balance):
        self.balance = balance
        self.lock = threading.Lock()

a = Account(1000)
b = Account(2000)

def transfer(from_acc, to_acc, amount):
    # 线程 A: 先锁 a,再锁 b
    # 线程 B: 先锁 b,再锁 a
    with from_acc.lock:
        with to_acc.lock:
            if from_acc.balance >= amount:
                from_acc.balance -= amount
                to_acc.balance += amount

# 线程 A: transfer(a, b, 100)
# 线程 B: transfer(b, a, 200)
# → A 锁住了 a,等待 b
# → B 锁住了 b,等待 a
# → 死锁!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

死锁的四个必要条件 ​

┌─────────────────────────────────────────────────────────────┐
│              死锁的必要条件(必须同时满足)                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 互斥:资源只能被一个线程持有                        │
│  2. 持有并等待:线程持有资源 A,同时等待资源 B           │
│  3. 不可抢占:资源不能被强制夺取                         │
│  4. 循环等待:线程 T1 等 T2,T2 等 T1                  │
│                                                             │
│  破坏任意一个条件 → 打破死锁                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12

打破死锁:按固定顺序加锁 ​

python
def transfer(from_acc, to_acc, amount):
    # 固定顺序:总是先锁 ID 小的账户
    first, second = sorted([from_acc, to_acc], key=lambda x: id(x))

    with first.lock:
        with second.lock:
            if from_acc.balance >= amount:
                from_acc.balance -= amount
                to_acc.balance += amount
1
2
3
4
5
6
7
8
9

第3节:线程池——参数怎么调 ​

问题:线程池参数设置不当 ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  场景:秒杀系统,1000 QPS                                 │
│                                                             │
│  方案 A:每请求一个线程                                   │
│  → 1000 个线程 → 线程创建/销毁开销 → 系统卡死         │
│                                                             │
│  方案 B:线程池过大                                       │
│  → 1000 个线程 → 上下文切换 → CPU 利用率下降           │
│                                                             │
│  方案 C:线程池过小                                       │
│  → 10 个线程 → 1000 QPS → 队列积压 → 超时              │
│                                                             │
│  问题:线程池到底应该设多大?                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

线程数公式 ​

┌─────────────────────────────────────────────────────────────┐
│              线程池大小计算公式                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  CPU 密集型(计算为主):                                │
│  线程数 = CPU 核心数 + 1                                 │
│  → CPU 核心数 = 8 → 线程数 = 9                          │
│  → 理由:线程可能在等待 CPU(上下文切换),多 1 个备用   │
│                                                             │
│  IO 密集型(网络/磁盘为主):                            │
│  线程数 = CPU 核心数 × (1 + IO 时间 / CPU 时间)         │
│  → CPU = 8 核心,IO/CPU = 4 → 线程数 = 8 × 5 = 40     │
│  → 理由:线程等待 IO 时不占 CPU,CPU 可以切换到其他线程 │
│                                                             │
│  实际经验:                                              │
│  IO 密集型线程数 = 2 × CPU 核心数 ~ 100 × CPU 核心数  │
│                                                             │
│  秒杀场景(实际是 IO + 锁竞争):                       │
│  → 线程池 + Redis 分布式锁配合                           │
│  → 线程数 = 100-200 足够                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Java 线程池参数 ​

java
// Java 线程池:7 个参数
ExecutorService executor = new ThreadPoolExecutor(
    10,              // corePoolSize:核心线程数(最小保留)
    50,              // maximumPoolSize:最大线程数
    60L,             // keepAliveTime:空闲线程存活时间
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000),  // 队列容量
    Executors.defaultThreadFactory(),   // 线程工厂
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
1
2
3
4
5
6
7
8
9
10

线程池的四种拒绝策略 ​

┌─────────────────────────────────────────────────────────────┐
│              线程池拒绝策略                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. AbortPolicy(默认):                                  │
│  → 抛出 RejectedExecutionException                       │
│  → 适用:需要严格控制并发数的场景                        │
│                                                             │
│  2. CallerRunsPolicy:                                    │
│  → 由调用方线程执行任务                                  │
│  → 适用:需要任务不被丢弃的场景(但可能拖慢调用方)     │
│                                                             │
│  3. DiscardPolicy:                                       │
│  → 直接丢弃任务                                          │
│  → 适用:不重要的任务                                    │
│                                                             │
│  4. DiscardOldestPolicy:                                │
│  → 丢弃队列中最老的任务,执行新任务                      │
│  → 适用:需要优先处理最新任务的场景                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

队列的选择 ​

┌─────────────────────────────────────────────────────────────┐
│              队列选择对比                                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  LinkedBlockingQueue(无界):                             │
│  → 队列无限增长,可能 OOM                                │
│  → 适用:不希望任务被拒绝的场景                         │
│                                                             │
│  LinkedBlockingQueue(有界):                             │
│  → 队列满了,新任务被拒绝                                │
│  → 适用:需要控制内存的场景                              │
│                                                             │
│  SynchronousQueue(同步队列):                           │
│  → 不存储任务,每个任务必须立即被取走                    │
│  → 适用:高并发 + 拒绝策略配合(触发快速失败)           │
│                                                             │
│  ArrayBlockingQueue(有界,数组):                       │
│  → 有界队列,性能更好                                    │
│  → 适用:固定并发数的场景                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

第4节:协程——为什么 Go 能轻松处理百万并发 ​

问题:线程太重了 ​

线程:
• 创建成本高(1-2MB 栈空间)
• 上下文切换慢(操作系统调度)
• 10000 个并发 = 10000 个线程 = 内存爆炸

协程:
• 创建成本低(几 KB 栈空间)
• 用户态调度(不涉及系统调用)
• 10000 个并发 = 10000 个协程 = 轻量级
1
2
3
4
5
6
7
8
9

Go 协程 vs Java 线程 ​

┌─────────────────────────────────────────────────────────────┐
│                 协程 vs 线程对比                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Java 线程:                                              │
│  → 1:1 模型:1 个线程 = 1 个 OS 线程                    │
│  → 上下文切换:用户态 ↔ 内核态,代价高                    │
│  → 栈大小:默认 1MB                                     │
│                                                             │
│  Go 协程:                                                │
│  → M:N 模型:M 个协程 → N 个 OS 线程                    │
│  → GMP 调度器:Goroutine → M 个 Machine → P 个 Processor  │
│  → 栈大小:初始 2KB,按需增长                           │
│  → 上下文切换:纯用户态,代价极低                         │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐ │
│  │  Go 调度器(GMP 模型):                           │ │
│  │                                                     │ │
│  │    G1 ──▶ P1 ──▶ M1                               │ │
│  │    G2 ──▶ P1 ──▶ M1                               │ │
│  │    G3 ──▶ P2 ──▶ M2                               │ │
│  │                                                     │ │
│  │  G = Goroutine  P = Processor  M = Machine (OS线程)│ │
│  └─────────────────────────────────────────────────────┘ │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

升华:并发问题的本质 ​

┌─────────────────────────────────────────────────────────────┐
│              并发问题的分类                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 竞态条件(Race Condition)                           │
│  → 多个线程同时读/写同一个变量                           │
│  → 解法:互斥锁 / CAS / 不可变对象                      │
│                                                             │
│  2. 死锁(Deadlock)                                     │
│  → 多个线程互相等待对方持有的锁                         │
│  → 解法:固定加锁顺序 / 锁超时 / 死锁检测                │
│                                                             │
│  3. 活锁(Livelock)                                     │
│  → 线程不断尝试但始终无法前进                           │
│  → 解法:随机退让                                        │
│                                                             │
│  4. 饥饿(Starvation)                                   │
│  → 某些线程始终得不到资源                               │
│  → 解法:公平锁                                          │
│                                                             │
│  核心原则:                                              │
│  → 能用不可变对象就不用锁                               │
│  → 能用乐观锁(CAS)就不用悲观锁(互斥)               │
│  → 必须用锁时,遵循固定顺序                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

"AI 可查 vs 必须理解"清单 ​

AI 可查:
✅ Java / Python 线程池 API 的具体参数
✅ ThreadPoolExecutor / ForkJoinPool 的使用场景
✅ Go GMP 调度器的详细配置

必须理解:
🔴 线程池队列满了,新任务怎么处理
🔴 CPU 密集 vs IO 密集的线程数计算公式
🔴 死锁的四个必要条件
🔴 竞态条件的本质(读-检查-写不是原子操作)
🔴 为什么 Go 协程比 Java 线程更轻量
1
2
3
4
5
6
7
8
9
10
11

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇2. 分布式系统——为什么单体时代过去了 / Distributed Systems Beyond the Monolith
下一篇4. MySQL 优化——为什么你的查询总是那么慢 / MySQL Query and Storage Optimization

持续记录,持续成长

Copyright © Tidenflow