打破并发代码

时间:2014-04-16 20:41:16

标签: java multithreading concurrency synchronized executorservice

假设我有以下课程:

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_INVNUM_OF_THREADS多次执行上述代码(方法),但从未设法获得NullPointerException

关于如何创建增加我在不更改BuggyClass的情况下获得异常的机会的测试的任何想法?

4 个答案:

答案 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

的原因