多线程卖票问题

多线程卖票问题

一、问题

假设共有固定数量的车票,开启多个线程模拟多个售票窗口同时售票,如果不做特殊处理,会出现同一张票被多次卖出、票数超卖的异常情况。

二、线程基础概念

线程是进程的运行子分支,同一个进程内的多个线程会共享进程中的数据。

线程安全问题的根源:比如多个线程同时修改同一份共享数据时,操作过程可能被 CPU 打断,最终导致数据结果出现问题。

三、创建线程的两种方式

方式 1:继承 Thread 类

自定义类继承 Thread 类

重写 run() 方法,编写线程要执行的业务逻辑

创建自定义类对象,调用 start() 方法启动线程

方式 2:实现 Runnable 接口

自定义类实现 Runnable 接口

重写 run() 方法,编写线程执行逻辑

将自定义类的对象传入 Thread 类的构造方法

调用 Thread 对象的 start() 方法启动线程

四、线程内存模型

堆内存:所有线程共享,静态变量、共享数据都存储在这里,比如总票数 count。

栈内存:每个线程私有,存放线程自己的局部变量。

线程操作共享数据时,会先把数据复制到自己的本地变量副本中,修改完成后再写回共享内存。普通的数值增减不是原子操作(执行中途可以被 CPU 打断),因此多线程同时操作就会出现数据错误。

线程内存模型示意图:

JVM

栈内存(线程私有)

堆内存(线程间共享)

线程 B

线程 A

count 副本

count

count 副本

副本--

副本--

五、线程安全解决方案:synchronized 内置锁

作用

将一段代码包裹起来,保证同一时刻只有一个线程能执行这段代码,让操作变成不可打断的原子操作。尽可能避免出现比如同一张票被多次卖出、票数超卖的异常情况

语法


java
synchronized(锁对象) {
// 需要保证原子性的核心代码
}


注意点

锁对象必须是所有线程都能访问到的同一个对象,通常用静态对象充当全局锁。

锁的粒度要尽量小,只包裹核心的共享数据操作代码;锁的范围太大,会导致其他线程长时间等待,降低程序运行效率。

线程进入同步代码块时自动获取锁,代码执行完毕后自动释放锁。

六、卖票代码核心逻辑

定义静态的总票数 count 和锁对象 obj,保证所有线程共享同一份数据和锁。

每个售票线程循环售票,在修改票数前加锁,并且加锁后再次判断票数是否大于 0,防止超卖。

减票操作完成后再输出售票信息,保证输出的数据准确。

小结

卖票问题的核心是多线程共享数据的安全问题,通过 synchronized 内置锁可以保证售票操作的原子性,有效避免超卖、重复售票的异常。