Java: волатильные гарантии подразумеваемого порядка - PullRequest
22 голосов
/ 09 ноября 2011

Мой вопрос является дополнением к этому: Изменчивые гарантии и исполнение вне порядка

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

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 из объявления состояния и все равно никогда не увидеть устаревшее значение. Вопросы:

  1. Правильно ли это рассуждение?
  2. Гарантируется ли, что поле для записи в инициализированное не будет оптимизировано (поскольку оно изменяется только в первый раз) и «барьер» не будет потерян?
  3. Предположим, вместо флага в качестве инициализатора использовался 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();
        }
    }
    

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

Ответы [ 2 ]

8 голосов
/ 09 ноября 2011

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

// 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.

В initialized есть 3 записи: #B, # 0 и # 2. Только № 2 присваивает истину. Поэтому, если № 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; это важно для обеспечения безопасной публикации: все потоки будут иметь правильно инициализированную защелку.

0 голосов
/ 09 ноября 2011

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

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

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

Да

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

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

...