【问题标题】:Breaking Concurrent Code破解并发代码
【发布时间】:2014-05-31 22:22:14
【问题描述】:

假设我有以下课程:

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();
    }

}

很容易看出,在上述代码的多线程执行中,我们可以在somethingElse 中得到一个NullPointerExeption。例如,可能是 failField != null 和在返回 failField.length() 之前 destroy 被调用,因此使 failField 变为 null

我想创建一个多线程程序,在使用BuggyClass 时能够“抛出”NullPointerException。我知道,由于该程序是多线程的,因此可能永远不会发生这种情况,但我想应该有一些更好的测试来增加发生异常的可能性。对吧?

我尝试了以下方法:

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 的情况下获得异常的机会的任何想法?

【问题讨论】:

  • 我认为failField == null 被认为是Java 中的单个操作。小手术不多。
  • 强制这样的条件非常困难(当然,除非你雇了一个傻瓜——傻瓜有时非常聪明)。
  • 不建议自己扔NullPointerException
  • String 在 Java 中是不可变的,因此它是线程安全的。任何不可变类都是线程安全的。
  • 伙计们:他试图导致(显然)错误代码失败。与字符串是不可变的还是不朽的,或者字符串引用上的== 是否是单个原子操作无关。

标签: java multithreading concurrency synchronized executorservice


【解决方案1】:

我很惊讶没有人注意到“failedField”没有以 volatile 关键字为前缀的事实。

虽然在 create() 方法中确实有可能发生竞争,但它可能在其他人的机器上工作的原因是“failedField”不在共享内存中并且缓存值为“ failedField”被使用。

此外,64 位机器上的引用并不像您想象的那样线程安全。这就是为什么AtomicReference exists in java.util.concurrent

【讨论】:

  • “64 位机器上的引用并不像你想象的那样线程安全”是什么意思。 Java 中的 AFAIK 引用在 64 位机器上与在 32 位机器上完全一样是线程安全的。
  • 啊哈!这全都与机器周期和指令可中断性有关。 CPU 能够中断一些运行时间较长的指令。在您发表评论之前,您是否真的检查了我上面提供的链接?
  • 我很想为您的最后一段设置为 -1,这暴露了对 Java 内存模型的严重误解。引用的读取和引用的写入都是线程安全和原子的,与机器无关; java.util.concurrent.AtomicReference 仅用于比较和设置(以及它所基于的更复杂的访问模式)。您链接到的答案中明确提到了这一点。
  • 我很抱歉 - 引用实际上在 JVM 规范中是原子的。然而,non-nolatile double 和 floats 不是原子的。见stackoverflow.com/questions/3463658/…
【解决方案2】:

它确实失败了……至少在我的机器上。问题是 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。

【讨论】:

    【解决方案3】:

    虽然您的代码中存在数据竞争,但可能无法看到由该数据竞争引起的任何问题。最有可能的是,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 被加载了一次。当然,不能保证所有实现现在和永远都使用相同的优化。所以,你不应该依赖它。

    【讨论】:

    • 谢谢,但我如何证明这一点?我可以检查创建的 java 字节码来验证是否是这种情况吗?
    • 这很容易用javap 验证。 (注意,如果编译器不这样做,理论上JVM/JITC是不允许的。)
    • @HotLicks:你能详细说明一下吗? (您指的是什么“理论”?)因为我非常确定 JVM/JITC 绝对允许进行这种转换。 . .
    • @HotLicks:我不明白你的评论。只要程序仍然按照 JLS 指定的方式运行,JIT 编译器就可以进行各种转换。
    • @user3542880:你为什么要证明,这段代码有问题?只是关于这段代码吗?或者您有兴趣了解一般的数据竞争问题吗?
    【解决方案4】:

    如果您只想查看问题,您可以在调用failField.length() 之前添加短睡眠,也可以在destroy() 方法中的failField = null 之后立即添加。这将扩大somethingElse() 方法访问null 状态变量的窗口。

    【讨论】:

    • 他特别提到BuggyClass不应该被修改。
    • 如果你不告诉我,我不会告诉任何人的。
    猜你喜欢
    • 2018-06-15
    • 2016-06-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-31
    相关资源
    最近更新 更多