面试回答

面试官你好,关于线程间同步的方式,主要有以下几种:

首先是互斥锁 (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