IntelliJ IDEA 2025.2 Help

教程:检测并发问题

与单线程应用程序中的错误相比,并发相关的错误通常更棘手,因为它们具有随机性。 一个应用程序可能会无故运行一千次,但随后出于未知原因突然失败。

在本教程中,我们将分析一个代码示例,该示例演示了调试和分析多线程应用程序的核心原理。

问题

一个常见的并发相关错误示例是竞争条件。 当多个线程同时修改共享数据而未正确同步时,就会发生这种情况。 只要两个线程中的读取和写入不重叠,这样的代码可能运行良好。

线程 1 在线程 2 开始读取时已经完成了写入

这种重叠可能非常罕见,从而让我们误以为代码中没有缺陷。 但是,当线程操作重叠时,数据会被破坏。

线程 2 读取值时,线程 1 还没有开始写入

如果我们不考虑这一点,就无法保证线程不会同时操作数据,尤其是当我们处理比单次读取和写入更复杂的内容时。 幸运的是,Java 具有内置的同步机制,确保一次只有一个线程处理数据。

让我们考虑以下代码:

import java.util.*; public class ConcurrencyTest { static final List a = Collections.synchronizedList(new ArrayList()); public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> addIfAbsent(17)); t.start(); addIfAbsent(17); t.join(); System.out.println(a); } private static void addIfAbsent(int x) { if (!a.contains(x)) { a.add(x); } } }

addIfAbsent 方法检查列表是否包含特定元素,如果不包含,则添加它。 我们从不同的线程调用此方法两次。 两次传递相同的整数值(17 ),并且由于保护条件((!a.contains(x)) ),只有第一个调用该方法的线程应该能够添加该值。 使用 SynchronizedList 是为了 防止 竞争条件。 最后, System.out.println(a) 语句打印出列表的内容。

如果我们长期使用此代码,会发现它有时仍会产生意外结果。

要找出原因,让我们检查代码的运行方式,看看我们是否真的设法防止了竞争条件。

复现该错误

使用 IntelliJ IDEA 调试器,您可以通过控制单个线程的执行而不是整个应用程序,测试应用程序的多线程设计并重现并发相关的错误。

  1. 在将元素添加到列表的语句处设置断点。

    在第 17 行设置了断点
  2. 将断点配置为仅挂起其被击中的线程。 这将确保两个线程在同一行暂停。 要执行此操作,请右键点击断点,然后点击 会话

    行断点弹出窗口中的 Thread 按钮
  3. 通过点击 运行 按钮(位于 main 方法附近)并选择 调试 开始调试会话。

    点击边栏中的 Run 图标时出现的弹出窗口

    当程序运行时,两个线程分别在 addIfAbsent 方法中被挂起。 现在,您可以在 线程 选项卡中切换线程并控制每个线程的执行。

    此时,两个线程都已经检查过列表中不包含 17 ,并准备将该数字添加到列表中。

  4. 线程 选项卡中,切换到 Thread-0

    Frames 选项卡中的 Thread 选择器
  5. 通过按下 F9 或点击 继续按钮 恢复线程,位于 调试 工具窗口的左侧。

    恢复 Thread-0 后,会继续将 17 添加到列表中,然后终止。 之后,调试器会自动切换回主线程。

  6. 恢复主线程以执行剩余的语句,然后终止。

  7. 请查看 控制台 选项卡中的程序输出。

    调试工具窗口的控制台选项卡

输出 [17, 17] 显示,两个线程能够绕过设置不正确的保护条件和同步,添加了相同的值。 我们使用调试器重现了事件的顺序,这表明存在竞争条件,我们需要纠正我们的方法。

修复程序

正如我们刚刚看到的,仅使用 SynchronizedList 是不够的。 它确保一次只有一个线程修改列表。 但是,我们仍然应该考虑到,检查 if (!a.contains(x)) 和修改 a.add(x) 不是一个原子操作。 因此,两个线程都能够评估条件并同时进入代码块。

让我们通过将条件包装在一个同步块中来修正代码。

private static void addIfAbsent(int x) { synchronized (a) { if (!a.contains(x)) { a.add(x); } } }

现在我们可以用修正后的代码重复该过程,并确保问题不再存在。

最后修改日期: 2025年 9月 22日