Как продемонстрировать проблемы многопоточности видимости java?

Если переменные в Java доступны из нескольких потоков, необходимо убедиться, что они безопасно опубликованы. Обычно это означает использование synchronized или volatile.

У меня сложилось впечатление, что некоторые из моих коллег серьезно не относятся к этой проблеме, так как они "никогда не слышали о volatile раньше и их программы работали годами".

Итак, мой вопрос:

Может ли кто-нибудь предоставить пример Java-программы/фрагмента, который надежно отображает проблемы видимости данных.

Я думаю, что запуск программы и просмотр неожиданного значения NPE или устаревшей переменной помогут больше, чем просто теоретические объяснения, которые не могут быть продемонстрированы.

Большое спасибо за вашу помощь!

Обновление: Просто для того, чтобы снова подчеркнуть этот момент. Я прочитал Java Concurreny на практике и знаю примеры, которые теоретически имеют проблемы с видимостью. Я ищу, чтобы на самом деле продемонстрировать их. Я не уверен, что это действительно возможно, но, возможно, есть конфигурация jvm или что-то подобное, что позволяет это.

Ответ 1

Изменив пример здесь, удалив операции, я привел пример, который последовательно терпит неудачу в моей среде (нить никогда не перестает работать).

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

Ответ 2

Может ли кто-нибудь предоставить пример Java-программы/фрагмента, который надежно показывает проблемы видимости данных.

Нет, нет надежного примера, который показывает проблемы видимости данных.

Причина в том, что любое допустимое выполнение программы с volatile также является допустимым исполнением одной и той же программы без volatile. (Противоположное, очевидно, не верно, хотя!)

Ответ 3

Наиболее распространенный пример, который подчеркивает важность использования 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.

Ответ 4

Заставьте их читать книгу Java Concurrency на практике от Java Concurrency гуру Брайана Гетца. Эта книга является обязательным для всех, кто должен написать любое серьезное параллельное программное обеспечение на Java!

Говоря "Я никогда не слышал о volatile, и мои программы работали годами" - это немой аргумент из невежества.

Ответ 5

У меня есть фрагмент кода для вас:

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 Server, сгенерировал этот вывод на моей машине:

..
..
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, что вряд ли произойдет, я думаю. Моя интерпретация заключается в том, что существует проблема видимости, которая заставляет потоки все еще читать done == false, хотя она уже обновлена ​​до true. После вызова синхронизированного метода System.out.println() с оператором "still working #" потоки прочитали обновленное значение для завершения и выхода.

Или кто-нибудь видит ошибку в моем коде/интерпретации?

Ответ 6

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.

Ответ 7

Расширение для кода @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() используется синхронизация. Подробное обсуждение ЗДЕСЬ