Нарушение Параллельного Кода


Допустим, у меня есть следующий класс:

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?

4 3

4 ответа:

Это не удается... по крайней мере, на моей машине. Дело в том, что Бегущий проглатывает исключение. Попробуйте вместо этого:

            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 каждый раз, когда я запускаю его.

Хотя в вашем коде есть гонка данных, может быть невозможно увидеть какие-либо проблемы, вызванные этой гонкой данных. Скорее всего, JIT-компилятор преобразует метод somethingElse в нечто подобное:

public long somethingElse() {
    String reg = failField; // load failField into a CPU register
    if (reg == null) {
        return -1;
    }
    return reg.length();
}

Это означает, что компилятор не будет загружать ссылку failField после выполнения условия. И невозможно вызватьNullPointerException .


Update: я скомпилировал Метод somethingElse с GCJ, чтобы увидеть некоторые реальные и оптимизированные выходные данные ассемблера. Выглядит это следующим образом:

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 загружается один раз. Конечно, нет никакой гарантии, что все реализации будут использовать одну и ту же оптимизацию сейчас и навсегда. Так что не стоит на это полагаться.

Если вы просто хотите увидеть проблему, вы можете добавить короткие сны перед вызовом failField.length(), а также сразу после failField = null в методе destroy(). Это расширило бы окно для метода somethingElse(), чтобы получить доступ к переменной в состоянии null.

Я удивлен, что никто не заметил тот факт, что "failedField" не был префиксом с ключевым словом volatile.

Хотя действительно существует возможность возникновения гонки в методе create (), причина, по которой он, вероятно, работает на машинах других людей, заключается в том, что "failedField" не был в общей памяти и вместо него использовалось кэшированное значение "failedField".

Кроме того, ссылки на 64-битные машины не так надежны, как вы думаете. Вот почему атомарность существует на Яве.утиль.параллельный