Прочитать устаревшее значение поля после построения объекта

Я читаю книгу Брайана Гетца о параллелизме в Java на практике. Пункты 3.5 и 3.5.1 содержат утверждения, которые я не могу понять.

Рассмотрим следующий код:

public class Holder {
  private int value;
  public Holder(int value) { 
    this.value = value;
  }

  public void assertValue() {
    if (value != value) throw new AssertionError("Magic");
  }
}

class HolderContainer {
  // Unsafe publication
  public Holder holder;

  public void init() {
    holder = new Holder(42);  
  }
}

Автор утверждает, что:

  1. В Java конструктор объекта сначала записывает значения по умолчанию во все поля перед запуском конструктора подкласса.
  2. Следовательно, можно увидеть значение поля по умолчанию как устаревшее значение.
  3. Поток может увидеть устаревшее значение при первом чтении поля, а затем более актуальное значение в следующий раз, поэтому assertN может выдать AssertionError.

Итак, согласно тексту, с некоторыми неудачными временами возможно, что value = 0; и в следующий момент value = 42.

Я согласен с пунктом 1, что конструктор объекта сначала заполняет поля значениями по умолчанию. Но я не понимаю пунктов 2 и 3.

Давайте обновим код авторов и рассмотрим следующий пример:

public class Holder {
  int value;

  public Holder(int value) {
    //Sleep to prevent constructor to finish too early
    try {
     Thread.sleep(3000);
    } catch (InterruptedException e) {
     e.printStackTrace();
    }
    this.value = value;
  }

  public void assertValue()  {
    if(value != value) System.out.println("Magic");
  }
}

Я добавил Thread.sleep (3000), чтобы заставить поток ждать, прежде чем объект будет полностью построен.

public class Tests {

  private HolderContainer hc = new HolderContainer();

  class Initialization implements Runnable {
    public void run() {
      hc.init();
    }
  }

  class Checking implements Runnable {
    public void run() {
      hc.holder.assertValue();
    }
  }

  public void run() {
    new Thread(new Initialization()).start();
    new Thread(new Checking()).start();
  }
}

Например:

  1. первый поток в своем объекте-держателе
  2. второй поток вызывает assertValue

Основной поток запускает два потока:

  1. новый поток (новая инициализация ()). start (); На создание объекта-держателя уходит 3 секунды.
  2. новый поток (новая проверка ()). start (); поскольку объект-держатель все еще не создан, код будет генерировать исключение NullPointerException

Следовательно, невозможно эмулировать ситуацию, когда поле имеет значение по умолчанию.

Мои вопросы:

  1. Автор ошибался насчет этой проблемы параллелизма?
  2. Или невозможно эмулировать поведение для значений полей по умолчанию?

person No Name QA    schedule 02.06.2018    source источник
comment
@VinceEmigh, тестировал на Java 6 и Java 8   -  person No Name QA    schedule 02.06.2018
comment
@AxelH, как я уже писал: новый поток (новая проверка ()). Start (); поскольку объект Holder все еще не построен, код вызовет исключение. Под этим я имел в виду, что у нас будет NPE.   -  person No Name QA    schedule 02.06.2018
comment
Но разве это Tests класс из книги? (извините, пропустил эти два пункта)   -  person AxelH    schedule 02.06.2018
comment
@AxelH, к сожалению, нет. Автор просто сделал состояния и дал нам два первых класса: Holder и HolderContainer. Так что тесты - это мой класс. С его помощью я пытаюсь проверить правильность утверждений автора.   -  person No Name QA    schedule 02.06.2018
comment
Тогда, возможно, потребуется проверить больше тестовых примеров. Понятно ;)   -  person AxelH    schedule 02.06.2018
comment
@AxelH Извини, не понял   -  person No Name QA    schedule 02.06.2018
comment
Возможный дубликат неправильной публикации справочника по объектам Java   -  person Dioxin    schedule 02.06.2018
comment
К сожалению, мне не удалось воспроизвести это самостоятельно. Тем не менее, я нашел еще один пост с вопросом об этой конкретной ситуации. К сожалению, мне не удалось найти в этом посте никаких примеров, воспроизводящих проблему. Достаточно интересно, что ответ там гласит: Насколько я знаю, в архитектуре x86 это невозможно, но может быть не так в других случаях.   -  person Dioxin    schedule 02.06.2018
comment
Это возможно только в том случае, если поток запускается изнутри конструктора - см. Здесь. Мне удалось вызвать исключение, используя пример из ссылки, но только когда я изменил утверждение на if (value != 42).   -  person Boaz    schedule 01.09.2018
comment
Автор прав, но получить эту ошибку утверждения может быть довольно сложно. Вы можете пробовать эти тесты снова и снова во многих потоках одновременно без ожидания, и даже тогда это может не сработать, потому что компилятор может оптимизировать проверку value != value так, чтобы она всегда была ложной. Тем не менее автор прав. Многие ошибки параллелизма появляются только тогда, когда вам не повезло. Кроме того, @Boaz неверен, когда говорит, что проблема возможна только в том случае, если поток просматривается в конструкторе. Другие потоки могут не видеть записи в том же порядке, в котором они были сделаны.   -  person Matt Timmermans    schedule 27.07.2020
comment
Честно говоря, даже не думайте о тестировании в условиях гонки. Возможно, вы захотите попробовать нагрузочное тестирование системы и автоматический рандомизированный запуск, но не думайте, что вы собираетесь писать тест для определенных условий гонки. Мне пришлось написать демонстрационные условия гонки, которые вызвали эксплуатируемые уязвимости безопасности в библиотеке классов Java. Это действительно неприятно, и это действительно плохие тесты. [1/2]   -  person Tom Hawtin - tackline    schedule 29.07.2020
comment
Тесты состояния гонки основаны на очень специфических деталях реализации, требуют времени на выполнение и неприятны для написания. (Раньше я доводил до такой степени демонстрацию, что ошибка обнаруживалась в течение примерно пяти минут на машине, которую я использовал.) Не говоря уже о том, чтобы связывать ваших самых лучших программистов (ION, любой получил работу на Программист Java в Эдинбурге?). Честно говоря, лучше использовать автоматические инструменты и не быть умным. [2/2]   -  person Tom Hawtin - tackline    schedule 29.07.2020


Ответы (4)


Я попытался проверить проблему с помощью следующего кода.

Тестовое задание:

public class Test {
    public static boolean flag =true;
    public static HolderContainer hc=new HolderContainer();

    public static void main (String args[]){    
        new Thread(new Initialization()).start();
        new Thread(new Checking()).start();
    }
}

class Initialization implements Runnable {
    public void run() {
        while (Test.flag){
            Test.hc=new HolderContainer();
            Test.hc.init();
            }
    }
}

class Checking implements Runnable {
    public void run() {
        try{
            Test.hc.holder.assertValue();
        }
        catch (NullPointerException e) {
        }    
    }
}

Держатель:

public class Holder {
    private int value;
        public Holder(int value) { 
        this.value = value;
    }

    public void assertValue() {
        if (value != value) {
            System.out.println("Magic");
            Test.flag=false;
        }
    }
}

class HolderContainer {
    public Holder holder;
    public void init() {
        holder = new Holder(42);  
    }
}

У меня никогда не было программы для оценки value!=value до true. Я не думаю, что это что-то доказывает, и не запускал его более пары минут, но я надеюсь, что это будет лучшей отправной точкой для хорошо разработанного теста или, по крайней мере, поможет выяснить некоторые возможные недостатки в тестах.

Я пытался вставить засыпание между Test.hc=new HolderContainer(); и Test.hc.init();, между public Holder holder; и public void init() { и после public void init() {.

Я также обеспокоен тем, что проверка того, является ли значение null или обнаружение NullPoiterException, может слишком сильно повлиять на синхронизацию.

Обратите внимание, что в принятом в настоящее время ответе на Неправильная публикация Справочника по объектам Java говорится, что эта проблема, вероятно, невозможна в архитектуре x86. Он также может зависеть от JVM.

person Community    schedule 06.06.2020
comment
Мое мнение - я не очень уверен - это то, что причина, по которой это невозможно на x86 Arch, заключается в том, что существует оптимизация компилятора, которая заменяет 'value! = Value' на false. ИЗМЕНИТЬ - извините, теперь я вижу, что в связанном вопросе обсуждается другой аспект чтения и записи. - person Rade_303; 06.06.2020
comment
Я пробовал немного с -Djava.compiler = NONE и со значением! = 42. Не уверен, что это поможет. Также на всякий случай вот байт-код для сравнения: 0: aload_0 / 1: getfield # 7/4: aload_0 / 5: getfield # 7/8: if_icmpeq 23. Из того, что я получил, компилятор не упрощает. . - person ; 07.06.2020
comment
@ Rade_303 Насколько я понимаю, оптимизация может играть важную роль. Исходная проблема, похоже, связана со спецификацией, не гарантирующей, что эта проблема не произойдет, а не с этой проблемой, которая действительно возникает в какой-то конкретной реализации, и воспроизведение этой проблемы кажется единственной, возможно забавной и / или полезной причиной, чтобы сохранить это как отдельный вопрос, и возиться с оптимизацией jvm кажется законным для этой задачи. Это требует знаний на самых разных уровнях, и я не самый подходящий для этого человек, и любая помощь и / или разделение проблемы приветствуются. - person ; 07.06.2020

Вероятно, вы пытаетесь смоделировать сценарий параллелизма, который, как мне кажется, очень сложно смоделировать с помощью пары потоков.

Следующий тестовый пример, который вы написали, совершенно неверен, с большей вероятностью выдаст NullPointerException.

public class Tests {

  private HolderContainer hc = new HolderContainer();

  class Initialization implements Runnable {
    public void run() {
      hc.init();
    }
  }

  class Checking implements Runnable {
    public void run() {
      hc.holder.assertValue();
    }
  }

  public void run() {
    new Thread(new Initialization()).start();  
    new Thread(new Checking()).start(); 
  }
}

Что, если ваш проверочный поток выполняется до инициализации? Кроме того, перевод в режим сна просто означает, что выполняющийся поток будет спать, и сообщит вам о внутренних атомарных операциях, выполняемых к тому времени.

person Yati Sawhney    schedule 02.06.2018
comment
Мой вопрос о том, можно ли получить значение по умолчанию, а затем значение по умолчанию. Как я уже упоминал в своем вопросе, мой код дает NullPoinnterExtecion, что на 100% логично. Но Автор заявляет, что каким-то образом можно получить значение! = Значение case. - person No Name QA; 02.06.2018
comment
Да, это возможно в реальном сценарии, когда у вас есть миллионы запросов. - person Yati Sawhney; 02.06.2018
comment
И пожалуйста, не делайте этого буквально (миллионы запросов), все приложения используют пулы для обработки запросов - person Yati Sawhney; 02.06.2018
comment
Итак, вопрос в том, как это воспроизвести? - person No Name QA; 02.06.2018
comment
Увеличьте количество потоков, обращающихся к одному и тому же объекту. Для тестирования вы можете поднять его до ~ 300-500. Также быстро отключите его, используя команду terminal / cmd javac & java, и запишите все в файл журнала. Независимо от того, будут ли какие-то исключения. С таким количеством ниток вы сможете воспроизвести его. - person Yati Sawhney; 02.06.2018
comment
В книге показано выражение value != value, которое сбивает с толку. Я не понимаю, как этот ответ объясняет такую ​​возможность. Этот ответ просто повторяет то, что сказано в книге: Теоретически возможно. Вы действительно предполагаете, что его тест недостаточно сложен, но, опять же, это не приближает нас ни к ответу, ни к возможности имитировать среду, которая могла бы воспроизвести это. - person Dioxin; 02.06.2018

Не воспроизводил это с вашим кодом. Вот пример имитации небезопасной публикации. Стратегия заключается в том, чтобы позволить одному потоку опубликовать Holder и позволить другому проверить его значение.

class Holder {
    private volatile int value;

    public Holder(int value, HolderContainer container) {
        container.holder = this;  // publication this object when it is not initilized properly
        try {
            Thread.sleep(10);  
        } catch (Exception e) {

        }
        this.value = value; // set value
    }

    public int getValue() {
        return value;
    }
}

class HolderContainer {

    public Holder holder;

    public Holder getHolder() { 
        if (holder == null) { 
            holder = new Holder(42, this);
        }
        return holder;
    }
}


public class Tests {

    public static void main(String[] args) {
        for (int loop = 0; loop < 1000; loop++) {
            HolderContainer holderContainer = new HolderContainer();
            new Thread(() -> holderContainer.getHolder()).start();
            new Thread(() -> {
                Holder holder = holderContainer.getHolder();
                int value1 = holder.getValue();  // might get default value
                try {
                    Thread.sleep(10);
                } catch (Exception e) {

                }
                int value2 = holder.getValue(); // might get custom value
                if (value1 != value2) {
                    System.out.println(value1 + "--->" + value2);
                }
            }).start();
        }
    }

}
person xingbin    schedule 02.06.2018
comment
Я не уверен, что этот пример подходит; здесь порядок операций обратный. Проблема согласованности из книги связана с тем, что потоки видят операции в другом порядке. Здесь, очевидно, можно увидеть значение = 0 в потоке чтения, даже если два потока согласовывают порядок, в котором выполняются операции. - person Daniele; 02.06.2018
comment
@Daniele Я не могу сказать, что это на 100% подходит. Я разместил это, потому что считаю, что речь идет о безопасной публикации. Если сообщество сочтет это бессмысленным, я удалю его. - person xingbin; 02.06.2018
comment
Небезопасная публикация @NengLiu - вещь 100% логичная. Мой вопрос касается случая, когда конструктор Holder super () (т.е. конструктор объекта) уже выполнен, а конструктор Holder все еще нет. В этом случае автор заявляет, что мы могли увидеть устаревшее значение = 0. И мне кажется, что невозможно воспроизвести такое поведение. - person No Name QA; 02.06.2018

Сон за 3 секунды перед назначением поля в конструкторе не имеет значения, потому что для того, чтобы value != value было true, первое чтение value должно привести к другому результату, чем второе, которое происходит сразу после.

Модель памяти Java не гарантирует, что значения, присвоенные полям в конструкторах, будут видны другим потокам после завершения работы конструктора. Чтобы получить эту гарантию, в поле должно быть указано final.

Вот программа, которая выдает ошибку на x86. Он должен запускаться с параметром виртуальной машины: -XX:CompileCommand=dontinline,com/mypackage/Holder.getValue

package com.mypackage;

public class Test {
    public static void main(String[] args) {
        new Worker().start();
        int i = 1;
        while (true) {
            new Holder(i++);
        }
    }
}

class Holder {
    private int value;

    Holder(int value) {
        Worker.holder = this;
        this.value = value;
    }

    void assertSanity() {
        if (getValue() != getValue()) throw new AssertionError();
    }

    private int getValue() { return value; }
}

class Worker extends Thread {
    static Holder holder = new Holder(0);

    @Override
    public void run() {
        while (true) {
            holder.assertSanity();
        }
    }
}

Запрещая встраивание Holder#getValue(), мы предотвращаем сворачивание двух последующих чтений value в одно.

Эта оптимизация предотвращает появление ошибки в коде книги. Однако автор книги по-прежнему прав, поскольку эта оптимизация не является обязательной, поэтому с точки зрения модели памяти Java код неверен.

Метод assertSanity() равен:

int snapshot1 = getValue();
                            // <--- window of vulnerability, where the observed value can change
                            //      if you chose to sleep 3 seconds, you would want to do it here
                            //      takes very little time, less than 1 nanosecond
int snapshot2 = getValue();
if (snapshot1 != snapshot2) throw new AssertionError();

Таким образом, первое чтение value может дать значение по умолчанию int, равное 0 (называемое устаревшим значением и присвоенное в конструкторе Object()), а второе чтение может дать значение, назначенное в конструкторе Holder(int). Это могло бы произойти, если бы, например, значение, присвоенное в конструкторе, было передано потоку, вызывающему assertSanity(), в точный момент между двумя загрузками value (окно уязвимости).

То же самое произойдет, если мы отложим второе чтение другим способом, например:

int snapshot1 = this.value;
Thread.interrupted();
int snapshot2 = this.value;
if (snapshot1 != snapshot2) throw new AssertionError();
person spongebob    schedule 27.07.2020