证明以下代码不是线程安全的

12

如何通过编写一些代码快速证明以下类不是线程安全的(因为它使用了惰性初始化并且没有使用同步)? 换句话说,如果我正在测试以下类的线程安全性,如何让它失败?

public class LazyInitRace {
  private ExpensiveObject instance = null;

  public ExpensiveObject getInstance() {
     if (instance == null)
        instance = new ExpensiveObject();
    return instance;
  }
}

在构造函数中添加“Thread.sleep”会有帮助吗? - Amarghosh
2
你必须使用代码来证明吗?还是可以通过其他方式证明?一个简单的执行图可以用来证明它不安全。 - Herms
有什么需要证明的吗?这不是线程安全的。 - ChaosPandion
同意Herms的观点,这似乎是一种简单情况,你甚至不需要“证明”它。 - matt b
4
明显的事实并不意味着证明它没有价值。线程和线程安全可能很复杂,有时候能够证明即使像这样简单的情况也是有用的,这样可以帮助教育那些新接触并发问题的人们。 - Herms
@Herms - 有时候我会忘记对于初学者来说,线程编程是多么困难。 - ChaosPandion
8个回答

15

根据定义,竞态条件不能被确定性地测试,除非您控制线程调度器(您无法控制)。您能做的最接近的事情是在getInstance()方法中添加可配置的延迟,或者编写可能出现问题并在循环中运行成千上万次的代码。

顺便说一下,这些都不构成“证明” 。形式验证可以,但即使对于相对较少的代码,这也非常困难。


3
任何证明它失败的演示都会证明它可能失败,因此我认为在这里使用“proof”没有任何问题。 - Herms
2
同意。反例证明是一种有效的证明方法。 - Peter Recore
1
问题在于产生反例的第一种方法需要修改代码,而第二种方法则是运气问题。 - Michael Borgwardt
查看我的关于通过调试器安排线程的答案。对于简单情况来说,虽然粗糙但是有效。 - Robin

13

你能否强制ExpensiveObject在测试中花费很长时间来构建?如果可以,只需从两个不同的线程中调用getInstance()两次,在短时间内进行,以便第一个构造函数在第二次调用之前尚未完成。这将导致构造两个不同的实例,其中您应该会失败。

然而,使天真的双重检查锁定失败将更加困难...(即使在变量上没有指定volatile也是不安全的)。


1
@Jon Skeet:请原谅我的无知,但我想了解volatile的具体含义是什么? - Will Marcouiller
1
volatile是Java关键字,用于控制JVM如何处理线程之间的内存访问。 - Peter Recore

5

虽然这不是使用代码,但以下是我证明的示例。我忘记了执行图表的标准格式,但意思应该足够清楚。

| Thread 1              | Thread 2              |
|-----------------------|-----------------------|
| **start**             |                       |
| getInstance()         |                       |
| if(instance == null)  |                       |
| new ExpensiveObject() |                       |
| **context switch ->** | **start**             |
|                       | getInstance()         |
|                       | if(instance == null)  | //instance hasn't been assigned, so this check doesn't do what you want
|                       | new ExpensiveObject() |
| **start**             | **<- context switch** |
| instance = result     |                       |
| **context switch ->** | **start**             |
|                       | instance = result     |
|                       | return instance       |
| **start**             | **<- context switch** |
| return instance       |                       |

3

由于这是Java,您可以使用thread-weaver库向您的代码中注入暂停或中断,并控制多个执行线程。这样,您就可以获得一个缓慢的ExpensiveObject构造函数,而无需修改构造函数代码,正如其他人(正确地)建议的那样。


以前从未听说过这个库。看起来相当有趣。 - Herms

2

好的...这段代码的结果将会是false,而你期望的是true。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class LazyInitRace {

    public class ExpensiveObject {
        public ExpensiveObject() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
    }

    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance() {
        if (instance == null)
            instance = new ExpensiveObject();
        return instance;
    }

    public static void main(String[] args) {
        final LazyInitRace lazyInitRace = new LazyInitRace();

        FutureTask<ExpensiveObject> target1 = new FutureTask<ExpensiveObject>(
                new Callable<ExpensiveObject>() {

                    @Override
                    public ExpensiveObject call() throws Exception {
                        return lazyInitRace.getInstance();
                    }
                });
        new Thread(target1).start();

        FutureTask<ExpensiveObject> target2 = new FutureTask<ExpensiveObject>(
                new Callable<ExpensiveObject>() {

                    @Override
                    public ExpensiveObject call() throws Exception {
                        return lazyInitRace.getInstance();
                    }
                });
        new Thread(target2).start();

        try {
            System.out.println(target1.get() == target2.get());
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
        }
    }
}

如果我在构造函数中省略Thread.sleep(),那么它会返回true。 也许如果我运行它数千次,有时会得到false。 - Lydon Ch

0
在构造函数中放置一个非常长的计算:
public ExpensiveObject()
{
    for(double i = 0.0; i < Double.MAX_VALUE; ++i)
    {
        Math.pow(2.0,i);
    }
}

您可能希望将终止条件减小为Double.MAX_VALUE/2.0或者除以一个更大的数字,如果MAX_VALUE太长时间了。

0

您可以通过调试器轻松地证明它。

  1. 编写一个在两个单独的线程上调用getInstance()的程序。
  2. 在ExpensiveObject构造函数上设置断点。确保调试器只会挂起线程,而不是虚拟机。
  3. 当第一个线程在断点上停止时,请将其挂起。
  4. 当第二个线程停止时,您只需继续。
  5. 如果检查两个线程对getInstance()调用的结果,它们将引用不同的实例。

这种方法的优点在于,您实际上不需要ExpensiveObject,任何对象实际上都会产生相同的结果。 您只是使用调试器来安排特定代码行的执行,从而创建确定性结果。


-1

嗯,它不是线程安全的。 证明线程安全性是随机的,但相当简单:

  1. 使ExpensiveObject构造函数完全安全:

    synchronized ExpensiveObject(){ ...

  2. 在构造函数中放置代码,检查是否存在对象的另一个副本 - 然后引发异常。

  3. 创建线程安全方法以清除“instance”变量

  4. 将getInstance / clearInstance的顺序代码放置到循环中,以供多个线程执行并等待(2)中的异常


我认为他并不是在寻找使代码线程安全的方法,而是想找一种展示它不是线程安全的方式。 - Michael Borgwardt
@Michael Borgwardt 看看 (2) - 抛出异常SHOW了不安全性。我使用的所有其他同步技巧都是为了避免在已有代码中引入新问题。 - Dewfy
你的#1是无效的,它不会编译。"synchronized" 不是构造函数的有效修饰符。所有的构造函数本身都是线程安全的。除此之外,你提出的解决方案过于复杂了...只需要将那个静态的 "getInstance" 方法加上 synchronized 关键字即可。 - NateS

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接