如何通过编写一些代码来快速证明以下类是不是线程安全的(因为它使用了惰性初始化而不是使用同步)? 换句话说,如果我正在测试以下类的线程安全性,我该怎么办呢?
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
答案 0 :(得分:15)
根据定义,除非您控制线程调度程序(您没有),否则无法确定性地测试竞争条件。您可以做的最接近的事情是在getInstance()
方法中添加可配置的延迟,或者在可能显示的问题中编写代码并在循环中运行数千次。
答案 1 :(得分:13)
您可以强制ExpensiveObject
在测试中花费很长时间吗?如果是这样,只需从两个不同的线程中调用getInstance()
两次,在足够短的时间内第一个构造函数在第二次调用之前就不会完成。您将最终构建两个不同的实例,这是您应该失败的地方。
让天真的双重检查锁定失败会更难,请注意......(即使没有为变量指定volatile
也不安全。)
答案 2 :(得分: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 :(得分:3)
由于这是Java,您可以使用thread-weaver库在代码中注入暂停或中断并控制多个执行线程。通过这种方式,您可以获得一个缓慢的ExpensiveObject
构造函数而无需修改构造函数代码,就像其他人(正确地)建议的那样。
答案 4 :(得分:2)
嗯......这段代码的结果将是假的,你期望是真的。
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) {
}
}
}
答案 5 :(得分: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
或除以更大的数字。
答案 6 :(得分:0)
您可以使用调试器轻松证明这一点。
这样做的好处是你实际上并不需要ExpensiveObject,任何Object实际上都会产生相同的结果。您只是使用调试器来安排执行特定代码行,从而创建确定性结果。
答案 7 :(得分:-1)
嗯,这不是线程安全的。 线程安全性的证明是随机的,但相当简单:
使ExpensiveObject构造函数完全安全:
synchronized ExpensiveObject(){...
放置构造函数代码,检查是否存在另一个对象副本 - 然后引发异常。
创建线程安全方法以清除'instance'变量
将getInstance / clearInstance的顺序代码循环以供多个线程执行并等待(2)