IntelliJ IDEA 2025.3 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); } } }

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

2026年 1月 23日