Странное поведение потока Java, связанного с System.out
У меня простой TestThreadClientMode
класс для проверки состояния гонки. Я попробовал две попытки:
- Когда я запускаю следующий код с
System.out.println(count);
прокомментировал во втором потоке, вывод был:
OS: Windows 8.1
flag done set true
...
и вторая нить была жива вечно. Потому что второй поток никогда не видит изменения done
флаг, который был установлен истинным главным потоком.
Когда я оставил комментарий
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();