面试题:基于Semaphore实现双线程同步(递增与显示线程)
嘿,这个问题我当年面后端开发岗的时候也被面试官追问过,刚好可以给你梳理清楚前因后果和解决方案!
问题背景
原题需求:同步两个线程:线程T1负责递增数值x(初始值x=5),线程T2仅在x被更新为新值时显示该数值(比如x从5→6时显示6一次,x→7时显示7一次,绝对不能重复显示同一个值)。我最初给出了用Semaphore的实现方案,面试官紧接着追问:如果发生线程上下文切换,该如何处理?
核心问题分析
首先得明确:用Semaphore做基础同步时,上下文切换可能引发两个典型的竞态问题:
- 问题1:T1在更新x和释放Semaphore之间被切走,导致T2被唤醒时读取的还是旧的x值
- 问题2:T2在显示完x后,还没准备好等待下一次通知时被切走,T1多次释放Semaphore后,T2可能一次性读取多个更新值,破坏“仅显示一次新值”的要求
针对上下文切换的修复方案(基于Semaphore的优化)
解决的核心思路只有一个:把「数值更新」和「同步信号操作」绑定成不可拆分的原子操作,同时确保T2的显示逻辑和信号量获取严格对应,彻底避免上下文切换带来的脏读或重复唤醒。
方案1:锁+Semaphore组合(最稳妥的常规实现)
用一个可重入锁(比如Java的ReentrantLock)来保护x的读写,确保T1更新x和释放Semaphore的操作是原子的;同时T2在获取Semaphore后,必须持有锁才能读取x,避免读取到未完成更新的脏值。
给个Java代码示例(其他语言可以类比实现):
import java.util.concurrent.Semaphore; import java.util.concurrent.locks.ReentrantLock; public class ThreadSyncDemo { private int x = 5; // 初始时T2没有可获取的许可,因为x还未更新 private final Semaphore updateSemaphore = new Semaphore(0); private final ReentrantLock valueLock = new ReentrantLock(); // 线程T1:负责递增x class IncrementThread extends Thread { @Override public void run() { try { // 模拟5次递增操作 for (int i = 0; i < 5; i++) { valueLock.lock(); try { x++; // 原子更新x的值 updateSemaphore.release(); // 释放许可,通知T2有新值 } finally { valueLock.unlock(); // 无论如何都要释放锁 } // 故意加休眠,模拟上下文切换的触发场景 Thread.sleep(100); } } catch (InterruptedException e) { // 正确处理中断 Thread.currentThread().interrupt(); } } } // 线程T2:仅在x更新时显示新值 class DisplayThread extends Thread { @Override public void run() { try { // 对应T1的5次递增 for (int i = 0; i < 5; i++) { updateSemaphore.acquire(); // 等待T1的更新通知 valueLock.lock(); try { System.out.println("更新后的x值:" + x); } finally { valueLock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public static void main(String[] args) { ThreadSyncDemo demo = new ThreadSyncDemo(); // 先启动T2,确保它先进入等待状态 new demo.DisplayThread().start(); new demo.IncrementThread().start(); } }
为什么这个方案能应对上下文切换?
- 锁的原子性保障:就算T1在更新x后刚要释放Semaphore时被上下文切换切走,其他线程(包括T2)也无法读取到x,因为锁还没释放,彻底避免了脏读
- Semaphore的严格计数:Semaphore的许可数严格对应T1的递增次数,就算T2被切走,许可数只会累计正确的次数,T2每次获取一个许可就对应一次新值显示,不会出现重复显示
方案2:用Condition替代Semaphore(更贴合场景的实现)
如果用Java的话,其实用Lock+Condition比Semaphore更贴合“等待更新通知”的场景,本质也是通过原子性操作解决上下文切换问题:
// 替换Semaphore为Condition private final Condition updatedCondition = valueLock.newCondition(); // T1的run方法中,更新x后替换为: updatedCondition.signal(); // T2的run方法中替换为: valueLock.lock(); try { updatedCondition.await(); // 等待T1的更新通知 System.out.println("更新后的x值:" + x); } finally { valueLock.unlock(); }
这个方案的好处是更直观,signal()只会唤醒一个等待的线程(也就是T2),避免Semaphore可能出现的许可累计问题,同样能完美处理上下文切换带来的竞态。
面试官追问的深层意图
其实面试官问上下文切换的问题,本质是在考察你对同步操作的原子性和竞态条件的理解,而不是单纯的Semaphore用法——你需要证明你知道:同步逻辑不能拆分成多个独立步骤,否则上下文切换会破坏整个同步逻辑的正确性。
内容的提问来源于stack exchange,提问作者arkham knight

