假设我有以下课程:
public class BuggyClass {
private String failField = null;
public void create() {
destroy();
synchronized (this) {
failField = new String("Ou! la la!");
}
}
public void destroy() {
synchronized (this) {
failField = null;
}
}
public long somethingElse() {
if (failField == null) {
return -1;
}
return failField.length();
}
}
很容易看出,在上述代码的多线程执行中,我们可以在NullPointerExeption
中获得somethingElse
。例如,failField != null
可能会在返回failField.length()
destroy
之前被调用,从而将failField
转换为null
。
我想创建一个多线程程序,在使用NullPointerException
时能够“抛出”BuggyClass
。我知道,由于程序是多线程的,可能是这种情况从未发生过,但我想应该有一些更好的测试可以增加获得异常的可能性。正确?
我尝试了以下内容:
final BuggyClass bc = new BuggyClass();
final int NUM_OF_INV = 10000000;
int NUM_OF_THREADS = 5;
ExecutorService executor = Executors.newFixedThreadPool(3 * NUM_OF_THREADS);
for (int i = 0; i < (NUM_OF_THREADS); ++i) {
executor.submit(new Runnable() {
public void run() {
for(int i = 0; i< NUM_OF_INV; i++){
bc.create();
}
}
});
}
for (int i = 0; i < (NUM_OF_THREADS); ++i) {
executor.submit(new Runnable() {
public void run() {
for(int i = 0; i< NUM_OF_INV; i++){
bc.destroy();
}}
});
}
for (int i = 0; i < (NUM_OF_THREADS); ++i) {
executor.submit(new Runnable() {
public void run() {
for(int i = 0; i< NUM_OF_INV; i++){
bc.somethingElse();
}}
});
}
executor.shutdown(); executor.awaitTermination(1, TimeUnit.DAYS);
我使用不同的NUM_OF_INV
和NUM_OF_THREADS
多次执行上述代码(方法),但从未设法获得NullPointerException
。
关于如何创建增加我在不更改BuggyClass
的情况下获得异常的机会的测试的任何想法?
答案 0 :(得分:4)
虽然您的代码中存在数据竞争,但可能无法看到由此数据竞争引起的任何问题。最有可能的是,JIT编译器会将方法 somethingElse 转换为如下所示:
public long somethingElse() {
String reg = failField; // load failField into a CPU register
if (reg == null) {
return -1;
}
return reg.length();
}
这意味着,编译器不会在条件之后加载引用 failField 。并且无法触发 NullPointerException 。
更新:我已经用GCJ编译了方法somethingElse
,以查看一些真实的和优化的汇编程序输出。它看起来如下:
long long BuggyClass::somethingElse():
movq 8(%rdi), %rdi
testq %rdi, %rdi
je .L14
subq $8, %rsp
call int java::lang::String::length()
cltq
addq $8, %rsp
ret
.L14:
movq $-1, %rax
ret
您可以从此代码中看到,引用 failField 已加载一次。当然,并不能保证所有实现都会立即使用相同的优化。所以,你不应该依赖它。
答案 1 :(得分:1)
它至少在我的机器上失败了......问题是Runnable吞下了异常。请尝试改为:
executor.submit(new Runnable() {
public void run() {
for (int i = 0; i < NUM_OF_INV; i++) {
try {
bc.somethingElse();
} catch (NullPointerException e) {
e.printStackTrace();
}
}
}
});
每次运行都会得到NPE。
答案 2 :(得分:0)
如果您只想查看问题,可以在致电failField.length()
之前以及failField = null
方法中的destroy()
之后立即添加短暂睡眠。这会扩大somethingElse()
方法访问null
状态变量的窗口。
答案 3 :(得分:0)
我很惊讶没有人注意到“failedField”没有带有volatile关键字的前缀这一事实。
虽然确实存在在create()方法中发生竞争的机会,但是它可能在其他人的机器上工作的原因是“failedField”不在共享内存中并且缓存值为“使用failedField“。
此外,64位计算机上的引用并不像您想象的那样是线程安全的。这就是AtomicReference exists in java.util.concurrent
的原因