不加锁会导致超量售票,因remainingTickets--非原子操作;须用synchronized同步读-改-写,实例变量锁this、静态变量锁TicketSeller.class;wait/notifyAll需在同步块内配合while循环防虚假唤醒。

售票线程不加锁会卖出超量车票
多个 Thread 同时调用 sellTicket(),但没用 synchronized 保护共享的余票数(比如 remainingTickets),结果就是:明明只剩 1 张票,三个窗口同时读到 “1”,各自减 1,最终卖出 3 张——实际库存变负。这不是偶发,是必然竞争态。
根本原因在于:读-改-写(remainingTickets--)不是原子操作。JVM 会拆成「读值→计算→写回」三步,中间可能被其他线程打断。
- 必须把整个卖票逻辑(判断是否可卖 + 扣减)包进一个同步块或同步方法里
- 锁对象要唯一且稳定,推荐用
this(实例方法)或类对象(TicketSeller.class,静态场景) - 别锁局部变量或每次 new 出来的对象,那等于没锁
synchronized(this) 和 synchronized(TicketSeller.class) 的区别
前者锁的是当前实例对象,适合每个窗口对应一个独立 TicketSeller 实例的场景;后者锁的是类的 Class 对象,所有实例共用同一把锁,适合全局余票共享(比如单例票池)。
常见误用:用 synchronized 修饰非静态方法,却指望它能保护静态变量——不行。synchronized 方法锁的是调用它的对象,静态变量属于类,得用类锁才管用。
立即学习“Java免费学习笔记(深入)”;
- 如果余票是实例变量(每个窗口自己维护余票),用
synchronized实例方法即可 - 如果余票是
static int remainingTickets,就必须用synchronized(TicketSeller.class)或静态同步方法 - 混用两种锁(一部分线程用 this,一部分用 class)会导致锁失效,余票仍可能超卖
wait() / notifyAll() 配合 synchronized 实现“没票就等”
单纯加锁只能防超卖,但用户会看到“票已售罄”后立刻退出,没法等待新放票。要用 wait() 暂停线程、notifyAll() 唤醒所有等待者——但这两者必须在 synchronized 块内调用,否则抛 IllegalMonitorStateException。
典型陷阱是只用 notify():它只随机唤醒一个线程,万一唤醒的是刚买完票的线程,其他还在等的线程永远卡住。必须用 notifyAll(),让所有等待者重新竞争锁和条件。
-
wait()会释放锁,线程进入 WAITING 状态;notifyAll()不释放锁,只是发信号 - 检查条件必须用
while而非if,防止虚假唤醒(spurious wakeup) - 示例关键片段:
while (remainingTickets <= 0) { wait(); }
用 ReentrantLock 替代 synchronized 有什么实际好处?
大多数售票场景下,synchronized 完全够用,且更简洁。只有当你需要 tryLock()(避免死等)、可中断等待(lockInterruptibly())、或按条件分组唤醒(Condition)时,才值得切到 ReentrantLock。
但切换后容易漏掉 unlock() —— 必须在 finally 块里显式调用,否则锁永远不释放。而 synchronized 是 JVM 自动管理进出,不会忘。
- 不要为了“听起来更高级”换锁;先确认业务真有
tryLock或公平性需求 - 若用了
ReentrantLock,务必配对lock()/unlock(),且unlock()在finally中 - 性能差异在现代 JVM 上几乎可忽略,别被过时的“synchronized 慢”说法带偏
真正难的不是写对锁,而是想清楚“谁和谁共享什么状态”。余票是全局的?每个窗口独立?退票要不要参与同步?这些设计决策一旦定错,加再多层 synchronized 也救不回来。










