Java多线程同步锁卖票实战:synchronized与Lock的区别

山海之间
山海之间
发布时间:2026-10-13 | 4人浏览过
原创

在Java并发编程中,多线程共享数据是引发“脏数据”的主要原因。本文将通过经典的“卖票”实战项目,演示如何使用同步锁解决资源竞争问题。文章包含无锁情况下的错误结果分析、使用synchronized的正确实现方式,以及同步锁内部原理的图解,最后对比synchronized关键字与Lock接口的区别,为开发者提供完整的线程同步解决方案。

什么是线程同步?

当多个线程同时访问同一个资源(如共享变量)时,如果不进行协调,可能导致数据不一致。例如,多个窗口同时卖出同一张票。线程同步旨在确保在同一时刻,只允许一个线程访问该临界资源,从而保证数据的准确性和操作的原子性。

第1步:创建售票任务类(Runnable)

为了实现多线程模拟,我们需要创建一个实现Runnable接口的类。在这个类中,我们定义总票数,并在run方法中编写售票逻辑。为了保证线程安全,必须使用synchronized(this)对共享资源tickets进行加锁保护。

关键参数说明:

  • int tickets = 20:模拟总票数,作为共享变量。
  • synchronized (this):获取当前对象实例的监视器锁,确保同一时间只有一个线程执行花括号内的代码。
  • Thread.sleep(200):模拟人工售票耗时,便于观察多线程交错执行的效果。
public class Ticket implements Runnable{
    int tickets=20;//总共20张票
    @Override
    public  void run() {
        while (tickets>0) {
            synchronized (this) {
                if(tickets>0) {
                    //售票
                    try {
                        System.out.println(Thread.currentThread().getName()+"正在售票");
                        Thread.sleep(200);//模拟售票操作时间
                        tickets--;//完成售票
                        System.out.println(Thread.currentThread().getName()+"售票完成,"+"剩余"+tickets+"张票");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

第2步:编写测试启动类

在主类中,创建Ticket类的单一实例。随后创建三个线程,传入同一个Ticket实例,模拟三个窗口同时售票的场景。每个线程启动后,将独立执行run方法,但受同步锁约束。

public class Test {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
 
        //模拟三个窗口售票
        Thread t1=new Thread(ticket);
        t1.setName("窗口1");
        t1.start();
 
        Thread t2=new Thread(ticket);
        t2.setName("窗口2");
        t2.start();
 
        Thread t3=new Thread(ticket);
        t3.setName("窗口3");
        t3.start();
    }
}

运行结果对比分析

1. 加入同步锁后的正常结果

由于使用了synchronized,线程按顺序逐一执行。可以看到输出日志严格遵循“窗口X正在售票 -> 窗口X售票完成 -> 剩余N张票”的逻辑,且票数递减无误(20->19...->0),没有重复售卖或跳号现象。

窗口1正在售票
窗口1售票完成,剩余19张票
窗口1正在售票
窗口1售票完成,剩余18张票
...
窗口2正在售票
窗口2售票完成,剩余0张票

2. 不加同步锁的异常结果

如果移除synchronized块,多线程将无序抢占CPU资源。典型错误包括:
- 超卖:票数减到负数。
- 重卖:多张票被标记为同一编号售出,或者同一票数被多次打印出售出信息。
从日志可见,"窗口1完成"和"窗口3完成"可能产生相同的剩余票数,或者出现"剩余19张票"后紧接着又出现"剩余19张票",这显然是数据竞争导致的严重Bug。

窗口3正在售票
窗口1正在售票
窗口2正在售票
窗口1售票完成,剩余18张票
窗口1正在售票
窗口2售票完成,剩余19张票
...

synchronized与Lock锁的核心区别

虽然本例使用synchronized成功解决了问题,但在更复杂的并发场景中,开发者常会对比synchronized关键字与java.util.concurrent.locks.Lock接口的区别:

  1. 层级不同:synchronized是JVM层面的关键字,由虚拟机自动获取和释放锁;而Lock是API级别的工具类(如ReentrantLock),需要手动调用lock()和unlock()。
  2. 灵活性不同:Lock支持尝试获取锁(tryLock)、中断获取锁等高级功能,synchronized则只能阻塞等待。
  3. 性能差异:在高并发激烈竞争下,旧版JDK中synchronized性能较差,但经过版本迭代(偏向锁、轻量级锁优化后),两者性能差距已大幅缩小。通常建议优先使用synchronized,除非需要Lock提供的特殊控制能力。

以上就是Java多线程实现同步锁卖票实战项目的详细内容,更多关于Java并发编程中线程安全与锁机制的资料请关注本站其它相关文章!

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表