Java 中的竞态条件

10 Sept 2024 | 4 分钟阅读

Java 是一种多线程编程语言,因此发生竞态条件的风险较高。因为同一个资源可能会被多个线程同时访问并可能修改数据。我们可以说竞态条件是一种**并发 bug**。它与**Java 中的死锁**密切相关。在本节中,我们将实现**Java 中的竞态条件**。

什么是竞态条件?

关键部分(程序中访问共享内存的部分)被两个或多个线程并发执行的条件。这会导致程序行为不正确。

通俗地说,**竞态条件**可以定义为两个或多个线程为了获取某些共享资源而相互竞争的条件。

例如,如果线程 A 正在从链表中读取数据,而另一个线程 B 正在尝试删除相同的数据。这个过程会导致竞态条件,可能会导致运行时错误。

竞态条件有两种类型

  1. 读-修改-写
  2. 检查-然后-执行

“**读-修改-写**”模式表示多个线程首先读取变量,然后修改给定值,再将其写回该变量。让我们看下面的代码片段。

为什么会发生?

当两个或多个线程在没有适当同步的情况下操作同一个对象,并且它们的操作相互干扰时,就会发生这种情况。

竞态条件示例

假设有两个进程 A 和 B 在不同的处理器上执行。两个进程都试图并发调用 bankAccount() 函数。我们将传递给函数的共享变量的值是 1000。

考虑,A 调用 bankAccount() 函数,并将 200 作为参数传递。同样,进程 B 也调用 bankAccount() 函数,并将 100 作为参数传递。

结果如下所示

  • 进程 A 将 1100 加载到 CPU 寄存器。
  • 进程 B 将 1100 加载到其寄存器。
  • 进程 A 将 200 添加到其寄存器,结果将是 1300
  • 进程 B 将 100 添加到其寄存器,计算出的结果将是 1200
  • 进程 A 将 1400 存储在共享变量中,进程 B 将 1150 存储在共享变量中。

RaceConditionProgram.java

输出

Value for Thread After increment Thread-1 2
Value for Thread at last Thread-1 2
Value for Thread After increment Thread-3 3
Value for Thread at last Thread-3 1
Value for Thread After increment Thread-2 2
Value for Thread at last Thread-2 0

在上面的输出中,我们可以观察到变量 c 给出了错误的值。

如何避免?

避免竞态条件的两种解决方案如下。

  • 互斥
  • 同步进程

为了防止竞态条件,应该确保一次只有一个进程可以访问共享数据。这是我们需要同步进程的主要原因。

避免竞态条件的另一种解决方案是互斥。在互斥中,如果一个线程正在使用共享变量或线程,那么另一个线程将排除自己执行相同的操作。

让我们看一个关于此的 Java 程序。

AvoidRaceCondition.java

输出

Value for Thread After increment Thread-1 1
Value for Thread at last Thread-1 0
Value for Thread After increment Thread-3 1
Value for Thread at last Thread-3 0
Value for Thread After increment Thread-2 1
Value for Thread at last Thread-2 0

从输出可以看出,现在线程一次访问一个共享资源。在 run() 方法中同步访问实现了这一点。