多窗口售票 Java 实例:避免竞争条件,实现安全售票
2023-10-01 14:12:59
引言
在软件开发中,我们经常会遇到需要解决竞争条件(race condition)的问题。竞争条件是指两个或多个线程同时访问共享资源时,由于无法确定各个线程的执行顺序,导致资源的访问结果不确定。在本文中,我们将使用 Java 来解决一个经典的多窗口售票问题,并分析该问题中存在的竞争条件和解决方法。
多窗口售票示例
首先,我们先来看一个简单的多窗口售票示例。假设我们有一个售票系统,其中有多个窗口同时售票,每个窗口都有自己的售票员。每当有顾客前来购票时,售票员会从售票窗口取出一张票并卖给顾客。
public class TicketWindow {
private int ticketsAvailable;
public TicketWindow(int ticketsAvailable) {
this.ticketsAvailable = ticketsAvailable;
}
public synchronized int buyTicket() {
if (ticketsAvailable > 0) {
ticketsAvailable--;
return ticketsAvailable;
} else {
return -1;
}
}
}
public class Main {
public static void main(String[] args) {
TicketWindow window1 = new TicketWindow(10);
TicketWindow window2 = new TicketWindow(10);
Thread thread1 = new Thread(() -> {
while (true) {
int ticketsLeft = window1.buyTicket();
if (ticketsLeft == -1) {
break;
}
}
});
Thread thread2 = new Thread(() -> {
while (true) {
int ticketsLeft = window2.buyTicket();
if (ticketsLeft == -1) {
break;
}
}
});
thread1.start();
thread2.start();
}
}
在上面的示例中,我们创建了两个售票窗口,每个窗口都有 10 张票。然后,我们创建了两个线程来模拟顾客购票。每个线程不断地从售票窗口取出一张票,直到没有票可卖为止。
多窗口售票问题中的竞争条件
当我们运行上面的程序时,可能会出现以下问题:
java.lang.RuntimeException:
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.ArrayIndexOutOfBoundsException:
at TicketWindow.buyTicket(TicketWindow.java:17)
at Main.lambda$main$0(Main.java:17)
at java.base/java.lang.Thread.run(Thread.java:834)
这个错误表明,在 buyTicket()
方法中,ticketsAvailable
变量的值为负数,导致数组越界。这是因为两个线程同时访问了 ticketsAvailable
变量,并且同时减少了它的值。由于 Java 中线程的执行顺序是不确定的,因此可能会出现一种情况:两个线程同时将 ticketsAvailable
的值减少到 0,然后其中一个线程又将它的值减少到 -1。
使用同步解决竞争条件
为了解决这个问题,我们需要对 buyTicket()
方法进行同步,以确保只有一个线程可以同时访问 ticketsAvailable
变量。我们可以使用 synchronized
来实现同步。
public synchronized int buyTicket() {
if (ticketsAvailable > 0) {
ticketsAvailable--;
return ticketsAvailable;
} else {
return -1;
}
}
现在,当两个线程同时调用 buyTicket()
方法时,它们将被强制按顺序执行。这意味着,一个线程将先执行,而另一个线程将等待,直到第一个线程执行完毕。这样就避免了竞争条件的发生。
运行结果
当我们再次运行程序时,我们将看到以下输出:
0
1
2
...
9
0
1
2
...
9
现在,两个售票窗口可以同时售票,而不会出现竞争条件。
上锁运行结果
在 Java 中,synchronized
关键字可以用于对任何对象进行上锁。这意味着,我们可以对任何对象进行同步,以确保只有一个线程可以同时访问它。
public synchronized void sellTicket() {
if (ticketsAvailable > 0) {
ticketsAvailable--;
}
}
在这个示例中,我们对 this
对象进行了上锁。这是一种常见的做法,因为它可以确保只有一个线程可以同时访问该对象的所有方法和变量。
锁资源优化
在某些情况下,我们可能不需要对整个对象进行上锁。例如,如果我们只想要对某个特定的变量或方法进行同步,那么我们就可以只对该变量或方法进行上锁。
private synchronized int ticketsAvailable;
public void sellTicket() {
if (ticketsAvailable > 0) {
synchronized (this) {
ticketsAvailable--;
}
}
}
在这个示例中,我们只对 ticketsAvailable
变量进行了上锁。这可以提高程序的性能,因为它减少了对对象的锁定时间。
结语
在本文中,我们探讨了如何使用同步机制解决多窗口售票问题,以避免竞争条件,实现安全售票。我们回顾了示例中的 bug,并提供了优化后的解决方案。我们还讨论了如何对对象进行上锁,以及如何优化锁资源。这些知识对于软件开发人员来说非常重要,因为它可以帮助我们避免竞争条件的发生,并提高程序的性能。