为什么Java多线程模拟售票未出现重复售票、超卖等线程安全错误
现象成因
你当前没有出现重复售票/超卖问题,本质是运行环境的巧合,不代表代码是线程安全的,具体原因如下:
- 执行速度过快:
sell()方法的逻辑非常精简,单线程一次调用耗时极短,在CPU时间片分配的粒度下,单个线程很容易连续多次抢到执行权限,直到时间片耗尽才会切换到其他线程,所以你会看到输出里A、B线程经常连续输出多条售票记录,很少出现交叉冲突。 - 未触发指令重排和竞争条件:你的测试用例总票数只有50,三个线程各执行50次,整体执行轮次很少,刚好没有出现多个线程同时进入
if (number > 0)判断的场景。 - 解释执行阶段未触发优化:如果代码运行时间更长,JIT编译后会进行指令重排,没有同步修饰的
number变量的可见性、操作原子性问题会更容易暴露。
复现线程安全问题的方法
你只需要给sell()方法加入微小的时间延迟,放大竞争窗口,立刻就能看到重复售票、超卖的问题:
public void sell() { if (number > 0) { // 加入10ms延迟,模拟业务处理耗时,放大线程竞争窗口 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " selled " + number-- + ", remain " + number); } }
正确的线程安全写法
如果要避免售票问题,需要给操作加同步,可选方案如下:
// 方案1:给sell方法加synchronized同步修饰 public synchronized void sell() { if (number > 0) System.out.println(Thread.currentThread().getName() + " selled " + number-- + ", remain " + number); } // 方案2:使用原子类保证number操作的原子性 class Ticket { private AtomicInteger number = new AtomicInteger(50); public void sell() { int cur = number.get(); if (cur > 0) { int remain = number.decrementAndGet(); if (remain >=0) { System.out.println(Thread.currentThread().getName() + " selled " + cur + ", remain " + remain); } } } }
内容的提问来源于stack exchange,提问作者Hao
相关产品推荐
相关产品推荐

