面试回答
面试官你好,关于线程间同步的方式,主要有以下几种:
首先是互斥锁 (Mutex),它是最基础的同步机制,保证同一时刻只有一个线程可以访问临界区。如果锁被占用,其他尝试获取锁的线程会被操作系统阻塞,让出 CPU。
其次是读写锁 (Read-Write Lock),这是对互斥锁的优化,适用于“读多写少”的场景。它允许多个线程同时获取读锁,但写操作是绝对互斥的。
第三种是条件变量 (Condition Variable),它主要用于解决线程间的“等待-通知”问题。当某个条件不满足时,线程会主动释放锁并挂起;当条件满足时,由其他线程唤醒它。条件变量必须和互斥锁配合使用。
第四种是自旋锁 (Spinlock),它和互斥锁类似,但区别在于当获取不到锁时,线程不会被挂起发生上下文切换,而是在一个循环中“忙等待”。它适用于临界区执行时间极短的多核场景。
最后是信号量 (Semaphore),它维护一个计数器,表示可用资源的数量。不仅可以用于互斥,更常用于控制对有限数量资源的并发访问。
在实际开发中,我们通常首选互斥锁和条件变量的组合,因为它们能应对绝大多数并发场景。在性能敏感且读多写少的情况下会考虑读写锁;而在内核态或极短时间的临界区,则会使用自旋锁。
系统讲解
1. 互斥锁 (Mutex)
互斥锁(Mutual Exclusion Lock)用于保护临界区,确保同一时刻只有一个线程能够执行临界区代码。
- 特点:独占式访问。获取不到锁的线程会被操作系统放入阻塞队列,让出 CPU,直到锁被释放后才会被唤醒。
- 适用场景:临界区执行时间较长,或者可能发生阻塞的操作。
- 缺点:线程阻塞和唤醒会带来上下文切换的开销。
2. 读写锁 (Read-Write Lock)
读写锁将对共享资源的访问分为读操作和写操作。
- 规则:
- 读读共享:多个线程可以同时获取读锁。
- 读写互斥:有线程持有写锁时,其他线程的读锁和写锁请求都会被阻塞。有线程持有读锁时,写锁请求会被阻塞。
- 写写互斥:同一时刻只能有一个线程持有写锁。
- 适用场景:读操作频繁,写操作较少的场景(如缓存系统)。
- 注意点:容易产生“写饥饿”问题(读锁不断被获取,导致写锁一直处于等待状态)。通常可以配置为“写优先”策略来缓解。
3. 条件变量 (Condition Variable)
条件变量允许线程在某个特定条件不满足时挂起,并在条件满足时被其他线程唤醒。
- 核心机制:必须与互斥锁配合使用。
wait:释放互斥锁,并将当前线程放入条件变量的等待队列中挂起。被唤醒后,会重新尝试获取互斥锁。signal/broadcast:唤醒等待队列中的一个或所有线程。
- 适用场景:生产者-消费者模型、任务队列同步。
// C++ 条件变量示例 (生产者-消费者)
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// 消费者线程
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
// 必须在 while 循环中 wait,防止虚假唤醒 (Spurious Wakeup)
cv.wait(lock, []{ return ready; });
// 执行消费逻辑
}
// 生产者线程
void producer() {
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // 唤醒一个消费者
}4. 自旋锁 (Spinlock)
自旋锁在获取不到锁时,不会让出 CPU 进入阻塞状态,而是通过一个 while 循环不断尝试获取锁(忙等待)。
- 特点:避免了线程上下文切换的开销,但会白白消耗 CPU 周期。
- 适用场景:临界区代码极短,且系统拥有多核 CPU。单核 CPU 下使用自旋锁没有意义,因为自旋的线程占用了 CPU,持有锁的线程根本没有机会执行来释放锁。
5. 信号量 (Semaphore)
信号量维护一个整型计数器,表示可用资源的数量。
- 操作:
P操作(Wait):如果计数器 > 0,计数器减一并继续执行;如果计数器 == 0,线程阻塞。V操作(Signal):计数器加一,并唤醒一个阻塞的线程。
- 分类:
- 二进制信号量:计数器只能是 0 或 1,功能类似于互斥锁。
- 计数信号量:计数器可以大于 1,用于控制多个同类资源的并发访问(如限制连接池的最大连接数)。
总结与对比
| 同步方式 | 核心机制 | 线程阻塞时是否让出 CPU | 适用场景 |
|---|---|---|---|
| 互斥锁 | 独占临界区 | 是(上下文切换) | 常规的临界区保护,执行时间较长 |
| 自旋锁 | 独占临界区 | 否(忙等待,消耗 CPU) | 临界区极短,多核环境,避免上下文切换开销 |
| 读写锁 | 读共享,写互斥 | 是 | 读多写少的场景(如缓存、配置读取) |
| 条件变量 | 等待特定条件满足 | 是 | 线程间的协调与通知(如生产者-消费者) |
| 信号量 | 资源计数器 | 是 | 控制对有限数量资源的并发访问(如连接池) |
常见追问:什么是虚假唤醒(Spurious Wakeup)?在使用条件变量时,线程可能会在没有其他线程调用
signal/notify的情况下被唤醒,或者被唤醒时条件其实已经不再满足(比如被其他并发线程抢先消费了资源)。解决方案:必须将
wait操作放在一个while循环中检查条件,而不是if语句中。被唤醒后,再次检查条件是否真正满足,如果不满足则继续wait。