Странное поведение потока Java, связанного с System.out

У меня простой TestThreadClientMode класс для проверки состояния гонки. Я попробовал две попытки:

  1. Когда я запускаю следующий код с System.out.println(count); прокомментировал во втором потоке, вывод был:

OS: Windows 8.1 flag done set true ...

и вторая нить была жива вечно. Потому что второй поток никогда не видит изменения done флаг, который был установлен истинным главным потоком.

  1. Когда я оставил комментарий System.out.println(count); вывод был:

    OS: Windows 8.1 0 ... 190785 190786 flag done set true Done! Thread-0 true

И программа остановилась через 1 секунду.

Как System.out.println(count); сделать второй поток увидеть изменения в done?

Код

public class TestThreadClientMode {
    private static boolean done;
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            public void run() {
                int count = 0;
                while (!done) {
                    count ++;
                    //System.out.println(count);
                }
                System.out.println("Done! " + Thread.currentThread().getName() + "  " + done);
            }
        }).start();
        System.out.println("OS: " + System.getProperty("os.name"));

        Thread.sleep(1000);
        done = true;

        System.out.println("flag done set true ");
    }
}

3 ответа

Решение

Это яркий пример ошибок согласованности памяти. Проще говоря, переменная обновляется, но первый поток не всегда видит изменение переменной. Эту проблему можно решить, сделав done переменная volatile объявив это так:

private static volatile boolean done;

В этом случае изменения в переменной видны всем потокам, и программа всегда завершается через одну секунду.

Обновление: кажется, что с помощью System.out.println действительно решает проблему согласованности памяти - это потому, что функция печати использует базовый поток, который реализует синхронизацию. Синхронизация устанавливает отношение "до того, как это произошло", как описано в связанном учебном пособии, которое имеет тот же эффект, что и переменная volatile. (Подробности из этого ответа. Также поблагодарите @Chris K за указание на побочный эффект операции потока.)

Как это сделал System.out.println(count); сделать второй поток увидеть изменения в done?

Вы наблюдаете побочный эффект println; ваша программа страдает от одновременного состояния гонки. При координации данных между процессорами важно сообщить программе Java, что вы хотите обмениваться данными между процессорами, в противном случае процессоры могут свободно задерживать связь друг с другом.

Есть несколько способов сделать это на Java. Основными из них являются ключевые слова "volatile" и "synchronized", которые вставляют в код то, что парни из аппаратных средств называют "барьерами памяти". Без вставки "барьеров памяти" в код поведение вашей параллельной программы не определено. То есть мы не знаем, когда "готово" станет видимым для другого процессора, и, таким образом, это состояние гонки.

Вот реализация System.out.println; обратите внимание на использование синхронизированных. Ключевое слово synchronized отвечает за размещение барьеров памяти в сгенерированном ассемблере, что побочным эффектом делает переменную "done" видимой для другого процессора.

public void println(boolean x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

Правильным решением проблемы для вашей программы является установка барьера для чтения при завершении чтения и барьера для записи при записи в него. Обычно это делается путем чтения или записи "done" из синхронизированного блока. В этом случае маркировка переменной done как volatile будет иметь такой же чистый эффект. Вы также можете использовать AtomicBoolean вместо boolean для переменной.

println() Реализация содержит явный барьер памяти:

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

Что заставляет вызывающий поток обновлять все переменные.

Следующий код будет иметь то же поведение, что и ваш:

    public void run() {
        int count = 0;
        while (!done) {
            count++;
            synchronized (this) {
            }
        }
        System.out.println("Done! " + Thread.currentThread().getName() + "  " + done);
    }

На самом деле любой объект может быть использован для монитора, следующие также будут работать:

synchronized ("".intern()) {
}

Другой способ создания явного барьера памяти - использование volatile, так что будет работать следующее:

new Thread() {
    private volatile int explicitMemoryBarrier;
    public void run() {
        int count = 0;
        while (!done) {
            count++;
            explicitMemoryBarrier = 0;
        }
        System.out.println("Done! " + Thread.currentThread().getName() + "  " + done);
    }
}.start();
Другие вопросы по тегам