Подтвердить что ты не робот

Java: неустойчивые подразумеваемые гарантии заказа

Мой вопрос - это расширение для этого: Неустойчивые гарантии и исполнение вне порядка

Чтобы сделать его более конкретным, скажем, у нас есть простой класс, который может быть в двух состояниях после его инициализации:

class A {
    private /*volatile?*/ boolean state;
    private volatile boolean initialized = false;

    boolean getState(){
        if (!initialized){
            throw new IllegalStateException();
        }
        return state;
    }

    void setState(boolean newState){
        state = newState;
        initialized = true;
    }
}

Инициализированное поле объявляется volatile, поэтому оно вводит "барьер", предшествующий "преждевременному", который гарантирует, что переупорядочение не может иметь место. Так как поле состояния записано только до, но и инициализированное поле записывается и читается только после, считывается инициализированное поле, я могу удалить ключевое слово volatile декларации государства и до сих пор не видят устаревшего значения. Вопросы:

  • Правильно ли это рассуждение?
  • Гарантировано ли, что запись в инициализированное поле не будет оптимизирована (поскольку она изменяется только в первый раз), а "барьер" не будет потерян?
  • Предположим, что вместо флага в качестве инициализатора был использован CountDownLatch:

    class A {
        private /*volatile?*/ boolean state;
        private final CountDownLatch initialized = new CountDownLatch(1);
    
        boolean getState() throws InterruptedException {
            initialized.await();
            return state;
        }
    
        void setState(boolean newState){
            state = newState;
            initialized.countdown();
        }
    }
    

    Будет ли все в порядке?

4b9b3361

Ответ 1

Ваш код (в основном) правильный, и это распространенная идиома.

// reproducing your code
class A

    state=false;              //A
    initialized=false;        //B

    boolean state;
    volatile boolean initialized = false;        //0

    void setState(boolean newState)
        state = newState;                        //1
        initialized = true;                      //2

    boolean getState()
        if (!initialized)                        //3
            throw ...;
        return state;                            //4

Строка #A #B - псевдокод для записи значений по умолчанию для переменных (ака обнуления полей). Мы должны включать их в строгий анализ. Обратите внимание, что #B отличается от # 0; оба выполняются. Строка #B не считается изменчивой записью.

Все волатильные обращения (чтение/запись) по всем переменным находятся в общем порядке. Мы хотим установить, что # 2 до 3 в этом порядке, если # 4 достигнуто.

Есть 3 записи в initialized: #B, # 0 и # 2. Только # 2 присваивает значение true. Поэтому, если # 2 после # 3, # 3 не может считаться истинным (это, вероятно, связано с отсутствием гарантии недостаточного уровня шума, который я не совсем понимаю), то №4 не может быть достигнуто.

Поэтому, если # 4 достигнуто, # 2 должно быть до # 3 (в общем порядке волатильных доступов).

Поэтому # 2 происходит - до # 3 (происходит летучая запись - перед последующим изменчивым чтением).

По порядку программирования, # 1 происходит - до # 2, # 3 происходит - до # 4.

По транзитивности, поэтому # 1 происходит - до # 4.

Строка # A, запись по умолчанию, происходит перед всеми (кроме других записей по умолчанию)

Поэтому все обращения к переменной state находятся в цепочке, которая происходит до: #A → # 1 → # 4. Нет гонки данных. Программа правильно синхронизирована. Чтение # 4 должно наблюдать запись # 1

Однако есть небольшая проблема. Строка № 0, по-видимому, избыточна, так как #B уже назначил false. На практике волатильная запись не является незначительной по производительности, поэтому нам следует избегать # 0.

Хуже того, наличие # 0 может вызвать нежелательное поведение: # 0 может появиться после # 2! Поэтому может случиться, что вызывается setState(), но последующие getState() продолжают бросать ошибки.

Это возможно, если объект не опубликован безопасно. Предположим, что поток T1 создает объект и публикует его; поток T2 получает объект и вызывает на нем setState(). Если публикация небезопасна, T2 может наблюдать ссылку на объект, прежде чем T1 завершит инициализацию объекта.

Вы можете игнорировать эту проблему, если вам требуется, чтобы все объекты A были безопасно опубликованы. Это разумное требование. Это может быть неявно ожидаемым.

Но если у нас нет строки # 0, это не будет проблемой вообще. Запись по умолчанию #B должна произойти - до # 2, поэтому, пока вызывается setState(), все последующие getState() будут наблюдать initialized==true.

В примере счетчика вниз, initialized есть final; что имеет решающее значение для обеспечения безопасной публикации: все потоки будут наблюдать правильно инициализированную защелку.

Ответ 2

1. Правильно ли это рассуждение?

Нет, состояние будет кэшироваться в потоке, поэтому вы не сможете получить последнее значение.

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

Да

3. Предположим, что вместо флага CountDownLatch использовался как        инициализатор вроде этого...

точно так же, как упоминается ураган @ratchet, CountDownLatch - это однократная задержка, а volatile - это многозадачная защелка, поэтому ответ на ваш третий вопрос должен быть: Если вы собираетесь установить состояние несколько раз вы должны использовать volatile.