双栈队列如何实现多线程安全?

考查重点:双栈搬运与队列操作互斥。用同一把锁保护入队、出队及输入栈到输出栈的搬运,确保另一个线程看不到搬运中间状态;空队列时按契约等待或返回

本题目录

参考答案

核心回答

给整个双栈队列使用同一把互斥锁,保护入队、查看和出队,并把“判断输出栈为空、搬运输入栈、弹出结果”放在一次持锁操作里。这样其他线程看不到搬运了一半的状态,队列操作可以按各次持锁的先后理解为顺序发生。

仅把两个栈换成线程安全容器,或者给两个栈分别加锁,都不足以保护跨栈的不变式。empty() 的返回值也只是调用时的快照,不能保证下一次 pop() 一定成功。

Python 实现

以下接口在空队列时立即抛错,不是阻塞队列。即使运行在带 GIL 的解释器中,也明确使用锁,不依赖多条操作碰巧不会被切换。

from threading import Lock

class SafeTwoStackQueue:
    def __init__(self):
        self.inbox, self.outbox = [], []
        self.lock = Lock()

    def push(self, value):
        with self.lock:
            self.inbox.append(value)

    def pop(self):
        with self.lock:
            if not self.outbox:
                while self.inbox:
                    self.outbox.append(self.inbox.pop())
            if not self.outbox:
                raise IndexError("empty queue")
            return self.outbox.pop()

    def peek(self):
        with self.lock:
            if not self.outbox:
                while self.inbox:
                    self.outbox.append(self.inbox.pop())
            if not self.outbox:
                raise IndexError("empty queue")
            return self.outbox[-1]

    def empty(self):
        with self.lock:
            return not self.inbox and not self.outbox

锁究竟保护什么

假设输出栈为空,线程甲开始从输入栈搬运;若线程乙此时直接弹输出栈,可能拿到最新元素而不是最早元素。因此搬运不是可以任意穿插的若干独立栈操作,而是队列出队的一部分。

peek() 与 pop() 一样,在输出栈为空时先把输入栈整体搬到输出栈,再查看输出栈顶;整个过程都在同一把锁内,不通过输入栈的底部索引绕过栈抽象。用户提供的耗时业务处理放在队列取值以后执行,不能占着队列锁运行。

pop() 或 peek() 触发的单次搬运可能持锁 O(n),所以其他线程可能等待较久。若题目继续要求空队列等待,可在相同锁上增加条件变量,使用 while 检查非空条件;若没有双栈这个实现约束,通常直接选成熟并发队列。

参考资料

  • 国内:JavaGuide:线性数据结构:采用栈与队列的先后顺序约束,说明单个栈操作不等于整个队列操作。
  • 国内:JavaGuide:集合常见问题(上):对照队列在空时抛错或返回特殊值的两类接口,明确示例的空队列行为,并与多线程快照查询区分。
  • 国内:JavaGuide:ArrayBlockingQueue源码分析:借鉴同一把锁保护队列状态和复合操作的方式;条件等待用于正文的阻塞扩展,示例本身仍采用空队列立即抛错的接口。
  • 国内:JavaGuide:并发常见问题(中):采用复合操作需要互斥与不能仅靠可见性的区分,界定临界区。
  • 国外:Python:threading:核对 Lock 保护复合队列操作的互斥与上下文管理;Condition 的等待语义仅用于正文的可选阻塞扩展。
  • 国外:Python:queue:核对空队列接口和状态查询只是瞬时快照的约束,解释先检查非空再出队为何仍存在竞态。

相关面经

1 篇

字节跳动

Agent 开发工程师
秋招一面
查看原面经 →
追问
后续追问: 如何在这个过程中实现多线程安全?

相关题目

4 题