Как продемонстрировать проблемы с многопоточностью в Java?
Если переменные в Java доступны из нескольких потоков, необходимо обеспечить их безопасную публикацию. Это обычно означает использование synchronized
или же volatile
,
У меня сложилось впечатление, что некоторые из моих коллег не воспринимают этот вопрос всерьез, поскольку они "никогда не слышали о volatile
раньше и их программы работали годами ".
Итак, мой вопрос:
Может ли кто-нибудь предоставить пример Java-программы / фрагмента, который надежно показывает проблемы с видимостью данных.
Я думаю, что запуск программы и обнаружение неожиданного значения NPE или устаревшей переменной поможет больше, чем просто теоретические объяснения, которые не могут быть продемонстрированы.
Большое спасибо за вашу помощь!
Обновление: просто чтобы еще раз подчеркнуть это. Я прочитал Java Concurreny на практике и знаю примеры, которые теоретически имеют проблемы с видимостью. То, что я ищу, это способ на самом деле продемонстрировать их. Я не уверен, что это действительно возможно, но, возможно, есть конфигурация jvm или что-то подобное, что позволяет это.
7 ответов
Изменяя приведенный здесь пример, удаляя операции, я пришел к примеру, который постоянно терпит неудачу в моей среде (поток никогда не останавливается).
// Java environment:
// java version "1.6.0_0"
// OpenJDK Runtime Environment (IcedTea6 1.6.1) (6b16-1.6.1-3ubuntu3)
// OpenJDK 64-Bit Server VM (build 14.0-b16, mixed mode)
public class Test2 extends Thread {
boolean keepRunning = true;
public static void main(String[] args) throws InterruptedException {
Test2 t = new Test2();
t.start();
Thread.sleep(1000);
t.keepRunning = false;
System.out.println(System.currentTimeMillis() + ": keepRunning is false");
}
public void run() {
while (keepRunning)
{}
}
}
Обратите внимание, что проблемы такого типа весьма зависят от компилятора / среды выполнения / системы. В частности, компилятор может решить добавить инструкции для чтения переменной из памяти, даже если она не является энергозависимой - так что код будет работать - vm и jit могут оптимизировать чтение из памяти и использовать только регистры, и даже процессор может переупорядочить инструкции - это не повлияет на этот случай, но в других многопоточных случаях это может повлиять на воспринимаемое состояние от других потоков, если будет изменено более одной переменной.
Наиболее распространенным примером, подчеркивающим важность использования volatile, является while(keepRunning)
пример:
public class Test extends Thread {
boolean keepRunning = true;
public static void main(String[] args) throws InterruptedException {
Test t = new Test();
t.start();
Thread.sleep(1000);
t.keepRunning = false;
System.out.println(System.currentTimeMillis() + ": keepRunning is false");
}
public void run() {
while (keepRunning)
System.out.println(System.currentTimeMillis() + ": " + keepRunning);
}
}
поскольку keepRunning
может (корректно) храниться в кэше потока, выполняющего цикл while, эта программа может вывести "true" для keepRunning
долго после keepRunning
установлено в false.
Обратите внимание, что нет надежного способа разоблачения условий гонки. (См. Мой другой ответ.) Этот пример может раскрыть его при определенных обстоятельствах на определенных комбинациях оборудования / операционной системы / jvm.
Может ли кто-нибудь предоставить пример Java-программы / фрагмента, который надежно показывает проблемы с видимостью данных.
Нет, надежного примера, показывающего проблемы с видимостью данных, не существует.
Причина в том, что любое действительное выполнение программы с volatile
также допустимое выполнение той же программы без volatile
, (Обратное, очевидно, не соответствует действительности!)
У меня есть фрагмент кода для вас:
package test;
public class LoopTest {
private boolean done = false;
/**
* @param args
*/
public void start() {
System.out.println(System.getProperty("java.vm.name"));
System.out.println(System.getProperty("java.version"));
for (int i = 0; i < 100; i++) {
startNewThread();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
done = true;
System.out.println("forcing end");
}
private void startNewThread() {
new Thread(new Runnable() {
public void run() {
long i = 0;
while(!done) {
i++;
if(i % 100L == 0) {
System.out.println("still working " + i);
}
}
System.out.println("ending " + i);
}
}).start();
}
public static void main(String[] args) {
new LoopTest().start();
}
}
Этот пример запуска в режиме сервера JVM сгенерировал этот вывод на моем компьютере:
..
..
ending 14100
still working 14800
ending 14800
still working 26500
ending 26500
still working 18200
ending 18200
still working 9400
ending 9400
still working 1300
ending 1300
still working 59500
ending 59500
still working 1700
still working 75400
ending 75400
still working 33500
ending 33500
still working 36100
ending 36100
still working 121000
ending 121000
still working 3000
ending 3000
ending 1700
still working 5900
ending 5900
still working 7800
ending 7800
still working 7800
ending 7800
still working 6800
ending 6800
still working 5300
ending 5300
still working 9100
still working 10600
ending 10600
still working 9600
ending 9600
still working 10000
ending 10000
ending 9100
still working 1700
ending 1700
..
..
Посмотрите на операторы "end #": все они имеют число, кратное 100, что, скорее всего, вряд ли произойдет. Моя интерпретация заключается в том, что существует проблема с видимостью, которая заставляет потоки по-прежнему читать готово == false, хотя оно уже было обновлено до true. После вызова синхронизированного метода System.out.println() с оператором "все еще работающий #" потоки считывают обновленное значение для done и завершают работу.
Или кто-нибудь видит ошибку в моем коде / интерпретации?
Предложите им прочитать книгу " Параллелизм на практике на Java" от гуру параллелизма на Java Брайана Гетца. Эта книга является обязательным для прочтения для любого, кто должен написать любое серьезное параллельное программное обеспечение на Java!
Конечно, говоря "Я никогда не слышал о volatile
и мои программы работали годами "- это глупый аргумент от невежества.
Расширение кода @David (конфигурация Java6 64bit, Eclipse Juno SR2):
public class NoVisibility_Demonstration extends Thread {
boolean keepRunning = true;
public static void main(String[] args) throws InterruptedException {
NoVisibility_Demonstration t = new NoVisibility_Demonstration();
t.start();
Thread.sleep(1000);
t.keepRunning = false;
System.out.println(System.currentTimeMillis() + ": keepRunning is false");
}
public void run() {
int x = 10;
while (keepRunning)
{
//System.out.println("If you uncomment this line, the code will work without the visibility issue");
x++;
}
System.out.println("x:"+x);
}
}
Используя этот пример, вы можете показать оба сценария. Когда вы раскомментируете строку в цикле while в run(), проблема видимости устраняется. Причина в том, что операторы println() используют синхронизацию. Подробное обсуждение ЗДЕСЬ
public class NoVisibility {
private static boolean ready = false;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) throws InterruptedException {
new ReaderThread().start();
number = 42;
Thread.sleep(20000);
ready = true;
}
}
Поместите Thread.sleep()
вызовите на 20 секунд, что произойдет, если JIT сработает в течение этих 20 секунд, и это оптимизирует проверку и кеширует значение или удалит условие в целом. И поэтому код не будет виден.
Чтобы этого не случилось, вы ДОЛЖНЫ использовать volatile
,