教程:检测并发问题
最后修改时间:2023 年 8 月 23 日本教程向您介绍如何使用 IntelliJ IDEA 调试多线程程序。
在编写多线程应用程序时,我们必须格外小心,因为我们可能会引入很难捕获和修复的错误。由于其随机性,与并发相关的错误比单线程应用程序中的错误更棘手。一个应用程序可能会完美地运行一千次,然后会因为没有明显原因而意外失败。
在本教程中,我们将分析一个代码示例,该示例演示了调试和分析多线程应用程序的核心原理。
问题
与并发相关的错误的一个常见示例是竞争条件。当多个线程同时修改某些共享数据时,就会发生这种情况。只要两个线程所做的修改不重叠,代码就可以正常工作。
这种重叠可能非常罕见,并导致我们认为代码中没有缺陷。但是,当线程操作重叠时,数据就会损坏。
如果我们不考虑这一点,就不能保证线程不会同时操作数据,特别是当我们处理比读写更复杂的事情时。幸运的是,Java 具有内置同步机制,可确保一次只有一个线程处理数据。
让我们考虑以下代码:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ConcurrencyTest {
static final List a = Collections.synchronizedList(new ArrayList());
public static void main(String[] args) {
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 调试器,您可以测试应用程序的多线程设计,并通过控制单个线程而不是整个应用程序来重现与并发相关的错误。
笔记
如果停止特定线程会破坏应用程序的操作,则表明存在设计缺陷。在稳健的设计中,无论事件时序如何,线程都能正确运行。
在将元素添加到列表的语句处设置断点。
配置断点以仅挂起断点所在的线程。这将确保两个线程在同一行挂起。为此,请右键单击断点,然后单击“线程”。
提示
如果您经常挂起单个线程,则可以单击“设为默认值”以使每个新断点仅挂起被命中的线程。
通过单击方法附近的“运行”
main
按钮并选择“调试”来启动调试会话。当程序运行时,两个线程都在该
addIfAbsent
方法中单独挂起。现在,您可以在线程之间切换(在“框架”或“线程”选项卡中)并控制每个线程的执行。此时,两个线程都已检查该列表不包含该数字
17
,并准备好将该数字添加到列表中。切换到
Thread-0
。通过按或单击“调试”工具窗口左侧部分来恢复线程。F9
在您恢复后
Thread-0
,它会继续添加17
到列表,然后终止。之后,调试器自动切换回主线程。恢复主线程以让它执行剩余的语句,然后终止。
在“控制台”选项卡中查看程序输出。
输出 ( [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);
}
}
}
现在,我们可以使用更正的代码重复该过程,并确保问题不再重现。
感谢您的反馈意见!