把十字路口看作临界资源,同时只能有一辆车进入。用一个显式队列缓冲所有到达的车辆,再用一个处理循环逐个放行,保护整个"判断信号灯 → 必要时变灯 → 通过路口"的流程。
维护一个变量记录当前哪条路是绿灯(初始为 Road A),再维护一个请求队列。当一辆车到达时:
carArrived 只做一件事:把请求加入队列。processNext)逐个取出请求执行,同一时刻只有一辆车在临界区。turnGreen() 并更新记录,然后调用 crossCar() 让车通过。JavaScript 是单线程事件循环,没有原生线程锁。"队列 + 单消费者"把并发到达的调用串行化,效果等价于 FIFO 互斥锁。
有一个十字路口,两条路相交:
roadId = 1):direction 1 为北向南,direction 2 为南向北。roadId = 2):direction 3 为西向东,direction 4 为东向西。每条路在进入路口前有一盏交通灯,可以是绿灯或红灯:
规则:
turnGreen() 将灯变绿(同时另一条路自动变红)。turnGreen():如果当前路已经是绿灯,再调用会被判错。你需要设计一个无死锁的交通灯控制系统。
多辆车可能同时到达路口。如果不加控制,会出现以下竞态:
因此,"判断信号灯 → 变灯(如果需要)→ 通过"这三步必须是原子操作,同一时刻只能有一辆车执行。
JavaScript 没有 synchronized 或 pthread_mutex,但它是单线程事件循环。只要确保前一个车的完整逻辑执行完,再执行下一个车,就能达到互斥效果。
这里用显式队列 + 处理标志实现:
要点有三个:
carArrived 不执行通过逻辑,只登记请求,调用方永远不被阻塞。processNext 一个地方执行"判断灯 → 变灯 → 通过",天然互斥;processing 标志保证任何时刻最多只有一个处理循环在跑。setImmediate 把下一步调度到下一个事件循环 tick,而不是直接递归调用。这样无论队列里积了多少车,调用栈都不会随队列深度增长(避免栈溢出),期间新到达的车也能随时入队。用 this.currentGreen 记录当前哪条路是绿灯。只有当 roadId !== this.currentGreen 时才调用 turnGreen(),满足题目"不能无故变灯"的要求。
| 阶段 | 对应代码 | 作用 |
|---|---|---|
| 记录绿灯方向 | this.currentGreen = 1 | 初始 Road A 绿灯,避免无故调用 turnGreen |
| 请求入队 | this.queue.push(...) | 到达即登记,调用方不被阻塞 |
| 启动保护 | if (!this.processing) | 保证任何时刻最多只有一个处理循环在跑 |
| 取出请求 | this.queue.shift() | 队首出队,FIFO 顺序处理 |
| 切换信号灯 | if (this.currentGreen !== roadId) | 只在需要变灯时才调用 turnGreen() |
| 更新记录 | this.currentGreen = roadId | 记录当前哪条路是绿灯 |
| 通过路口 | crossCar() | 在独占保护下执行通过操作 |
| 异步接力 | setImmediate(() => this.processNext()) | 下一步放到新的事件循环 tick,调用栈不随队列深度增长 |
| 排空退出 | this.processing = false | 队列清空后复位标志,下一辆车到达时能重新启动循环 |
processNext 一处,且 processing 标志保证任何时刻最多一个处理循环在跑,因此不会有两辆车同时处于临界区。this.currentGreen 记录当前绿灯方向。如果车所在路已经是绿灯,不会调用 turnGreen(),满足题目约束。turnGreen / crossCar 不会抛异常。若要防御回调抛异常导致 processing 卡在 true、后续车辆无人处理,可以把取出请求后的执行包进 try / finally,在 finally 中继续调度下一步。this.currentGreen 必须初始化为 1,否则第一辆 Road A 的车会错误地调用 turnGreen()。this.processing = false,之后到达的车会认为循环还在跑,永远等不到处理。shift 而不是 pop: 从队首取车才是 FIFO;从队尾取会变成后进先出,先到的车可能饥饿。processNext 末尾若直接调用自身,调用栈深度会随队列长度增长,大量车辆到达时栈溢出;用 setImmediate / setTimeout(..., 0) 把下一步调度到新 tick。turnGreen(): 必须通过 this.currentGreen 判断,当前路已经是绿灯时跳过。processNext 内完成,不能让 carArrived 各自执行。设总共到达 n 辆车:
O(n)。每辆车只执行一次临界区逻辑,锁的等待和释放都是 O(1)。O(n)。最坏情况下所有车同时到达,队列中会堆积 n 个待处理的请求。不够。在多线程/并发环境中,"判断绿灯 → 通过路口" 是两步操作。如果没有锁,可能多个线程同时判断为绿灯并同时通过,造成竞态。锁保证这两步原子化。
LeetCode 的测试框架会并发调用 carArrived(例如通过多个 Worker 或快速连续调用)。虽然 JS 引擎是单线程,但这些调用产生的异步回调可能交错执行。队列 + 处理标志把通过逻辑收敛到唯一的消费者循环中串行执行,避免状态判断和更新的竞态。
可以用 Promise 链,把每辆车的通过任务依次接到链尾:
Promise 的语义保证链上任务串行执行,逻辑与显式队列等价。写法更短,但"链即锁"不如"队列 + 处理循环"直观,而且任务抛异常会让链进入 rejected 状态、后续车辆被跳过。
turnGreen 或crossCar 是异步的,代码需要怎么改?如果它们是返回 Promise 的异步函数,把 processNext 改为 async,在调用处加 await,等异步操作完成后再调度下一步:
await 保证当前车的异步操作彻底完成后,下一辆车才开始处理。
是。队列用 shift 从队首取车,先调用 carArrived 的车先被处理,不会出现"后来的车先通过"的饥饿现象。
processing 复位为 false,之后到达的车辆永远无人处理。pop 代替 shift 取车,把公平队列变成后进先出。processNext 末尾直接同步递归调用自身,大量车辆时调用栈溢出。crossCar() 而没有保护 turnGreen() 和状态更新。currentGreen 设错,导致所有车都触发一次 turnGreen()。carArrived 都调用 turnGreen(),不管当前是否已经是绿灯。setImmediate 调度,调用栈深度与队列长度解耦。先只回答第 1 题,再展开后续问题:
this.currentGreen 的读取和修改也必须在 processNext 内完成?processNext 结尾用 setImmediate 调度下一步,而不是直接递归调用 this.processNext()?setTimeout 随机延迟调用 carArrived,队列版本还能保证正确性吗?为什么?carArrived,并分析与显式队列版本在异常处理上的差异。