【问题标题】:How to test thread safety of a class from multiple threads?如何从多个线程测试一个类的线程安全性?
【发布时间】:2015-12-16 18:50:18
【问题描述】:

我正在研究线程安全的单例类,我知道下面的代码不是线程安全的,因为我正在做一个双重检查锁定错误。

protected static TestSingleton instance;

private TestSingleton() {
    // some code
}

public static synchronized void setup() {
    if (instance == null) {
        TestSingleton holder = new TestSingleton();
        instance = holder;
    }
}

public static TestSingleton getInstance() {
    if (instance == null) {
        setup();
    }
    return instance;
}

但我正在尝试对此进行测试,以便我可以通过编程方式证明这不是线程安全类。这是为了我的学习经验。

如何以编程方式对其进行测试并证明它不是线程安全的?

【问题讨论】:

  • 您无法以编程方式证明代码是线程安全的 - 该代码实际上可能在您的 JVM/CPU 架构/操作系统的组合上始终可以正常工作。您只能希望测试会发现问题。
  • 如果你在 x86/hotspot 上运行它,你可能无法让它失败。这并不意味着它是安全的,它可能会在 Java 9 或 SPARC 或... 线程(非)安全性通过应用 JLS 的规则来证明。
  • 根据 Java 语言规范,请怀疑论者构造一个证明它线程安全的。我看到的问题是调用getInstance() 的线程可能会在该值完全构造之前“看到”分配给instance 的非空值。在这种情况下,读取线程永远不会遇到内存屏障。如果他们如此确信它是安全的,他们应该急于承担举证责任。
  • 不,不能保证它会失败。但是,如果您遵守规则,并且能够清楚地表达它们,那么您可以确定它不会失败。为什么这甚至是一个问题?为什么不使用正确的机制?谁对这个特定的实现如此执着?为什么?

标签: java multithreading thread-safety


【解决方案1】:

归结为:您的程序有一定数量的线程,在一定数量的处理器上运行,但您的计算机只有一个主内存。

每个线程都会以某种顺序读取和写入不同的内存位置,这在某种程度上独立于其他线程正在做的事情;所有这些读取和写入都必须在进入内存时进行序列化。也就是说,内存系统必须一个接一个地执行这些读取和写入。

对于多线程程序的任何给定执行,可以有无数不同的方式来序列化不同线程的操作。从字面上看,数不胜数。如果即使其中一种可能的序列化导致您的程序产生错误的输出,那么您的程序就不是线程安全的。

那么你如何测试它们呢?

你不能。它们太多了,并且可能在一台计算机(例如,您客户的关键任务服务器)上经常发生的序列化可能永远不会在其他计算机(例如,您的测试工具)上发生。

确保线程安全的唯一方法是使用经过数学​​验证的算法来同步线程的操作,这样就不会发生错误的交错。

【讨论】:

  • 不是线程安全的,因为某个线程 A 可以调用 getInstance() 并找到 instance != null,并且永远不会与线程 B 同步——创建单例的线程。如果没有同步,就不能保证线程 A 的内存视图与线程 B 的视图一致。特别是,理论上,线程 A 可以看到处于部分初始化或未初始化状态的单例对象,即使它看到 instance 已设置。谷歌“java发生在之前”以了解更多信息。
【解决方案2】:

简短回答:你不能。

无论您在测试环境中运行此代码多少次,它实际正常工作的可能性都非零。 问题是,您无法真正估计此概率,因此无法找到使它们显示正确结果的概率接近 1 所需的测试运行次数。

您实际上可以推断给定程序可能如何执行。找到让多线程程序中断的方法通常比证明它永远不会中断要容易。

在这种情况下,存在以下问题:在线程 1 中执行 setup 期间 写入instance读取 之间没有发生之前的关系em> from instance 执行以从线程 2 中的 getInstance 执行返回一个值,也没有执行来自 instanceread 以检查线程 2 中 setup 执行中的 null。意味着来自不同线程的两次调用可能会产生不同的结果,也就是说,实现被破坏了,我们甚至没有开始考虑代码重新排序。

例如,通过将instance 声明为volatile 来建立这种关系可以解决它。

您可以在 Alexey Shipilev 的 this excellent paper about Java Memory Model 中了解有关顺序一致性、程序顺序和发生前关系的更多信息。这将极大地帮助您。

不幸的是,目前无法实现上述推理的自动化。

【讨论】:

    【解决方案3】:

    我知道为什么我的以下回复不正确。请参阅相关评论线程了解原因。

    我认为不可能证明你的程序在当前版本中不是线程安全的,因为方法内部的逻辑足够“安全”。

    这是因为,即使可能会尝试同时调用 getInstance() 内部的 setup() 调用,但 setup() 内部的逻辑将同步发生,因为方法被声明为同步。

    假设您有两个线程,A 和 B,竞争对 setup() 的访问。 A 首先在实例变量中设置对 TestSingleton 对象的引用。当 A 在 setup() 方法中时,B 尝试调用它并被阻止。 A 完成,B 进入方法。由于 instance 现在不为空,B 不会调用 setup() 中的任何逻辑。因此,您将永远不会看到 setup() 中条件块内的逻辑的多次调用。

    【讨论】:

    • 这里的问题是new TestSingleton() 不是原子的。并且可以通过首先获取内存、分配instance 值然后才调用构造函数体来进行优化。这会导致某些线程可以看到TestSingleton处于未初始化状态
    • @talex 同步方法创建了一个发生前的关系,因此在它被输入一次之后进入它的任何线程都应该看到一个完全构造的 TestSingleton 对象。我的分析是否遗漏了什么?
    • @Andonaeus 是的:第二个线程可能会在 getInstance 中看到实例为非 null,因此 进入同步设置方法。所以你没有发生之前的关系。
    • happens-before 只有在第二个线程从同步块中读取分配的值时才能得到保证。但是 getInstance() 不同步。它读取该值,如果看到非空值,则跳过同步设置方法。
    • @JBNizet 啊,现在我明白了我错过了什么。感谢您与我在一起!
    【解决方案4】:

    您最有可能通过您描述的测试寻找的东西是两个线程从其getInstance() 方法获得TestSingleton 的不同实例,或者一个线程从该方法获得null 返回值,或者一个线程在完全初始化之前返回一个实例。

    问题在于,您无法确定其中任何一个发生的可能性有多大,实际上,它们可能非常不太可能。您可能会运行大量的测试迭代,而从未看到任何这些失败行为。如果您确实看到其中之一,那么您将证明该程序不是线程安全的,但没有看到其中任何一个说明什么。

    线程安全是你必须通过分析程序来证明的程序属性。无法通过有限时间的测试来证明。

    【讨论】:

    • @user1950349,任何对 Java 同步的分析最终都必须建立在对 Java 内存模型的要求之上,如 chapter 17 of the JLS 中所述。这绝不是一本容易阅读的书。对于为什么 Java 中的双重检查锁定被破坏,在网络上提供一些 许多 可信的解释可能会更容易。
    • @JohnBollinger 你是对的。我必须补充一点,为了获得对 Java 内存模型的入门级理解,我在回答中链接了 Alexey Shipilev 的一篇非常好的文章。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-04
    • 2020-09-07
    • 2010-10-31
    • 1970-01-01
    相关资源
    最近更新 更多