# 教程：检测并发问题

> For the complete documentation index, see [llms.txt](https://www.jetbrains.com.cn/help/idea/llms.txt).

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

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

## 问题

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

![线程 1 在线程 2 开始读取时已经完成了写入](https://resources.jetbrains.com.cn/help/img/idea/2026.3/debug_concurrency_issues_tutorial_1.png)

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

![线程 2 读取值时，线程 1 还没有开始写入](https://resources.jetbrains.com.cn/help/img/idea/2026.3/debug_concurrency_issues_tutorial_2.png)

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

让我们考虑以下代码：

```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)`  语句打印出列表的内容。

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

> **Tip:**
> 如果您感到好奇并且想重现这种意外行为，您可以创建一个测试，一直运行直到首次失败。

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

## 复现该错误

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

> **Note:**
> 如果停止特定线程会中断您的应用程序的操作，这表明存在设计缺陷。 在健壮的设计中，线程无论事件的时间安排如何，都能正确运行。

Procedure:

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

![在第 17 行设置了断点](https://resources.jetbrains.com.cn/help/img/idea/2026.3/debug_concurrency_issues_tutorial_procedure_1.png)

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

![行断点弹出窗口中的 Thread 按钮](https://resources.jetbrains.com.cn/help/img/idea/2026.3/debug_concurrency_issues_tutorial_procedure_2.png)

> **Tip:**
> 如果您经常挂起单个线程，可以点击   设为默认值  使每个新的断点仅挂起其命中的线程。

3. 通过点击   运行   按钮（位于  `main`  方法附近）并选择   调试   开始调试会话。

![点击边栏中的 Run 图标时出现的弹出窗口](https://resources.jetbrains.com.cn/help/img/idea/2026.3/debug_concurrency_issues_tutorial_procedure_3.png)

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

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

4. 在   线程   选项卡中，切换到  `Thread-0` 。

![Frames 选项卡中的 Thread 选择器](https://resources.jetbrains.com.cn/help/img/idea/2026.3/debug_concurrency_issues_tutorial_procedure_4.png)

5. 通过按下 `F9` (Windows), `⌘ ⌥ R` (macOS), `F9` (IntelliJ IDEA Classic (macOS)), `⌃ \` (macOS System Shortcuts), `F9` (XWin), `F9` (GNOME), `F9` (KDE), `F9` (Emacs), `F9` (Sublime Text), `F9` (Sublime Text (macOS)), `F5` (Visual Studio), `F5` (Visual Studio (macOS)), `F5` (NetBeans), `F8` (Eclipse), `F8` (Eclipse (macOS)) 或点击 ![“继续”按钮](https://resources.jetbrains.com.cn/help/img/idea/2026.3/app.actions.resume.svg) 恢复线程，位于   调试   工具窗口的左侧。

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

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

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

![调试工具窗口的控制台选项卡](https://resources.jetbrains.com.cn/help/img/idea/2026.3/debug_concurrency_issues_tutorial_procedure_5.png)

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

## 修复程序

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

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

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

```

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

