видимость неизменяемого объекта после публикации - PullRequest
2 голосов
/ 24 июля 2011

У меня есть неизменный объект, который помещен в класс и является глобальным состоянием.

Допустим, у меня есть 2 потока, которые получают это состояние, выполняем с ним myMethod (состояние). И давайте скажем, что thread1 заканчивает первым. Он изменяет глобальное состояние, вызывая GlobalStateCache.updateState (state, newArgs);

GlobalStateCache {
   MyImmutableState state = MyImmutableState.newInstance(null, null);

   public void updateState(State currentState, Args newArgs){
      state = MyImmutableState.newInstance(currentState, newArgs);
   }
}

Таким образом, thread1 обновит кэшированное состояние, затем thread2 сделает то же самое и переопределит состояние (не принимая во внимание состояние, обновленное из thread1)

Я искал спецификации google, java и на практике читал параллелизм java, но это явно не указано. Мой главный вопрос: будет ли значение объекта неизменяемого состояния видимым для потока, который уже прочитал неизменяемое состояние. Я думаю, что он не увидит измененное состояние, только читает после того, как обновление увидит его.

Так я не могу понять, когда использовать неизменяемые объекты? Это зависит от того, согласен ли я с одновременными изменениями во время работы с последним состоянием, которое я видел, и не нужно обновлять состояние?

Ответы [ 5 ]

4 голосов
/ 25 июля 2011

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

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

  • , когда вы говорите допустим, что thread1 заканчивает первым - как вы узнали бы это?или, если быть более точным, как thread2"узнает" это?насколько я могу судить, это могло бы быть возможно только с некоторой синхронизацией, явной или не такой явной, как в потоке join (см. JLS - 17.4.5 Порядок Happens-before * ).Код, который вы предоставили до сих пор, не дает достаточных подробностей, чтобы сказать, имеет ли это место

  • , когда вы заявляете, что thread1 обновит кэшированное состояние - как бы thread2"знаете" что?с предоставленным вами фрагментом кода для thread2 кажется совершенно возможным (но не гарантированным), что вы никогда не узнаете об этом обновлении

  • при указании thread2 ... переопределит состояние что здесь означает override ?В примере кода GlobalStateCache нет ничего, что могло бы как-то гарантировать, что thread1 когда-нибудь заметит это переопределение .Более того, предоставленный код не предлагает ничего, что каким-либо образом навязывало бы отношение «произойдет до» обновлений из разных потоков, так что можно даже предположить, что переопределение может произойти наоборот, понимаете?

  • последнее, но не менее важное, формулировка неизменное состояние звучит для меня довольно размыто.Я бы сказал опасно нечетко, учитывая эту сложную тему.Поле состояние является изменяемым, его можно изменить, хорошо, вызвав метод updateState верно?Из вашего кода я бы скорее пришел к выводу, что экземпляры класса MyImmutableState предполагаются неизменяемыми - по крайней мере, так говорит мне имя.

С учетом всего сказанного выше, чтогарантированно будет видимым с кодом, который вы предоставили до сих пор?Боюсь, не очень ... но, может быть, лучше, чем ничего.Я вижу это ...

Для thread1 гарантируется, что перед вызовом updateState он увидит либо null , либо правильно построенный (действительный ) объект обновлен из thread2.После обновления гарантированно виден любой из правильно построенных ( valid ) объектов, обновленных из thread1 или thread2.Обратите внимание, что после этого обновления thread1 гарантированно не увидит null для самого JLS 17.4.5, на который я ссылаюсь выше ( "... x и y - действия того же потока, и x предшествует yв программном порядке ... ")

Для резьбы 2 гарантии очень похожи на описанные выше.

По сути, все это гарантируется с помощью предоставленного вами кодав том, что оба потока будут видеть null или один из правильно построенных ( valid ) экземпляров MyImmutableState класса.

Выше гарантии могут выглядеть незначительнымина первый взгляд, но если вы просмотрите одну страницу выше страницы с цитатой, которая вас смутила («Неизменяемые объекты можно безопасно использовать и т. д.»), вы найдете более полезный пример в 3.5.1.Неправильная публикация: когда плохие объекты испортятся .

Да, объект, который сам по себе неизменен, не гарантирует его видимости, но, по крайней мере, гарантирует, что объект не "взорвется изнутри", как в примерепредусмотрено в 3.5.1:

public class Holder {
  private int n;

  public Holder(int n) { this.n = n; }

  public void assertSanity() {
    if (n != n)
      throw new AssertionError("This statement is false.");
  }
}

Комментарии Гетца для приведенного выше кода начинаются с объяснения истинных проблем как с изменяемыми, так и с неизменяемыми объектами,

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

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

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

Выше «AssertionHorror» может показаться нелогичным, но вся магия исчезнет, ​​если вы рассмотрите сценарий, подобный приведенному ниже (полностью допустимый для модели памяти Java 5 - и по хорошей причине, кстати):

  1. вызывает поток 1 sharedHolderReference = Holder (42);

  2. первый поток потока 1 заполняет n поле со значением по умолчанию (0) затем назначит его в конструкторе, но ...

  3. ... но планировщик переключается на thread2,

  4. sharedHolderReference из потока 1 становится видимым для потока 2, потому что, скажем, потому что почему бы и нет?возможно, оптимизирующий компилятор хот-спота решил, что сейчас самое подходящее время, чтобы

  5. thread2 считал обновление sharedHolderReference со значением поля, все еще равным 0 между прочим

  6. thread2 вызывает sharedHolderReference.assertSanity ()

  7. thread2 считывает значение левой стороны , если инструкция в assertSanity , который, ну, тогда он будет читать значение правой стороны, но ...

  8. ... но планировщик переключается обратноto thread1,

  9. thread1 завершает присвоение конструктора, приостановленное на шаге 2 выше, установив n значение поля 42

  10. значение 42 в поле n из потока 1 становится видимым для потока 2, потому что, скажем, почему бы и нет?может быть, оптимизирующий компилятор хот-спота решил, что это подходящее время для

  11. , а затем, через некоторое время, планировщик переключается обратно на thread2

  12. thread2 продолжается с того места, где он был приостановлен на шаге 6 выше, то есть он читает правую часть оператора if , что, в общем, 42 теперь

  13. упс, наша невинная if (n! = N) внезапно превращается в if (0! = 42) , что ...

  14. ... естественно выбрасывает AssertionError

Насколько я понимаю, безопасность инициализации для неизменяемых объектов просто гарантирует, что выше не будетбывает - не больше ... и не меньше

1 голос
/ 25 июля 2011

Я думаю, что ключ должен различать объекты и ссылки.

Неизменяемые объекты безопасны для публикации, поэтому любой поток может публиковать объект, а если любой другой поток читает ссылку на такой объект - он может безопасно использовать этот объект.Конечно, поток чтения будет видеть состояние неизменяемого объекта, которое было опубликовано в тот момент, когда поток читает ссылку, он не будет видеть никаких обновлений, пока не прочитает ссылку снова.,Например, если есть один издатель, и много читателей - и читатели должны видеть последовательное состояние.Читатели периодически читают ссылку и работают с полученным состоянием - оно гарантированно будет согласованным и не требует какой-либо блокировки потока читателя.Также, когда все в порядке, чтобы потерять некоторые обновления, например, вам все равно, какой поток обновляет состояние.

1 голос
/ 24 июля 2011

Если я понимаю ваш вопрос, неизменность здесь не актуальна .Вы просто спрашиваете, будут ли потоки видеть обновления для общих объектов.

[ Edit ] после обмена комментариями, теперь я вижу, что вам также нужно хранить ссылку на ваш общий синглтонсостояние при выполнении некоторых действий, а затем установка состояния, отражающего это действие.

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

Вместо этогодля определения отдельных синхронизированных методов getState и updateState вам придется выполнять все три действия без прерывания: getState, yourAction и updateState.

Я вижу три способа сделать это:

1) Выполнить все три шага внутри одного синхронизированного метода в GlobalStateCache. Определить атомарныйdoActionAndUpdateState метод в GlobalStateCache, синхронизированный, конечно, на вашем state синглтоне, который будет использовать объект-функтор для выполнения ваших действий.

2) Выполните getState и updateState как отдельные вызовы и измените updateState, чтобы он проверял, чтобы убедиться, что состояние не изменилось с момента получения. Определите getState иcheckAndUpdateState в GlobalStateCache.checkAndUpdateState примет исходное состояние вызывающего абонента, полученное из getState, и должно быть в состоянии проверить, изменилось ли состояние с момента вашего получения.Если изменилось , вам нужно будет что-то сделать, чтобы вызывающий абонент знал, что ему потенциально необходимо отменить свое действие (зависит от вашего варианта использования).

3) ОпределитеgetStateWithLock метод в GlobalStateCache .Это означает, что вам также необходимо убедиться, что вызывающие абоненты снимают блокировку.Я бы создал явный releaseStateLock метод, и ваш updateState метод вызвал бы его.

Из них я советую против # 3, потому что он оставляет вас уязвимым для того, чтобы оставить это состояние заблокированным в случаенекоторые виды ошибок.Я бы также посоветовал (хотя и не так сильно) против # 2 из-за сложности, которую он создает с тем, что происходит в случае изменения состояния: вы просто отказываетесь от действия?Вы пытаетесь это повторить?Должно ли это быть (это может быть) отменено?Я за # 1: одиночный синхронизированный атомарный метод , который будет выглядеть примерно так:

public interface DimitarActionFunctor {
  public void performAction();
}
GlobalStateCache {    
  private MyImmutableState state = MyImmutableState.newInstance(null, null);     
    public MyImmutableState getState {     
      synchronized(state) {
        return state;    
      }
    }     
    public void doActionAndUpdateState(DimitarActionFunctor functor, State currentState, Args newArgs){       
      synchronized(state) {
         functor.performAction();
         state = MyImmutableState.newInstance(currentState, newArgs);    
      }
    } 
  } 
} 

Затем Caller создает функтор для действия (экземпляр DimitarActionFunctor),и вызывает doActionAndUpdateState.Конечно, если действиям нужны данные, вам придется определить интерфейс функтора, чтобы эти данные принимались в качестве аргументов.

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

0 голосов
/ 25 июля 2011

Отвечая на ваш «главный» вопрос: нет Тема 2 не увидит изменения.Неизменяемые объекты не изменяются: -)

Таким образом, если Thread1 читает состояние A, а затем Thread2 сохраняет состояние B, Thread1 должен снова прочитать переменную, чтобы увидеть изменения.

На видимость переменных влияет ключевое слово volatile.Если переменная объявлена ​​как volatile, то Java гарантирует, что если один поток обновит переменную, все остальные потоки увидят это изменение немедленно (за счет скорости).

Все еще неизменные объекты очень полезны в многопоточных средах.Я приведу пример того, как я использовал его один раз.Допустим, у вас есть объект, который периодически изменяется (поле life в моем случае) одним потоком и каким-то образом обрабатывается другими потоками (моя программа отправляла его клиентам по сети).Эти потоки завершаются ошибкой, если объект изменяется в середине обработки (они отправляют несогласованное состояние жизненного поля).Если вы сделаете этот объект неизменяемым и будете создавать новый экземпляр каждый раз, когда он изменяется, вам вообще не нужно писать никакой синхронизации.Обновляющий поток будет периодически публиковать новые версии объекта, и каждый раз, когда другие потоки читают его, они получают самую последнюю версию и могут безопасно обрабатывать его.Этот конкретный пример экономит время, затрачиваемое на синхронизацию, но тратит больше памяти.Это должно дать вам общее представление о том, когда вы можете их использовать.

Еще одна ссылка, которую я нашел: http://download.oracle.com/javase/tutorial/essential/concurrency/immutable.html

Редактировать (ответить на комментарий):

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

  1. Поток, который обновляет объект, представляющий поле жизни.Он неизменен, поэтому фактически каждый раз создает новый экземпляр и только меняет ссылку.Тот факт, что ссылка объявлена ​​как volatile, имеет решающее значение.
  2. Каждый клиент обслуживается со своим собственным потоком.Когда клиент запрашивает поле жизни, он читает ссылку один раз и начинает отправку.Поскольку сетевое соединение может быть медленным, поле жизни может обновляться много раз, пока этот поток отправляет данные.Тот факт, что объект является неизменным, гарантирует, что сервер будет отправлять согласованное поле состояния жизни.В комментариях вы обеспокоены изменениями, внесенными в то время, когда этот поток обрабатывает объект.Да, когда клиент получает данные, они могут не обновляться, но с этим ничего нельзя поделать.Это не проблема синхронизации, а скорее проблема медленного соединения.

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

0 голосов
/ 24 июля 2011

Так много здесь зависит от фактического случая использования, что трудно дать рекомендацию, но похоже, что вы хотите какую-то семантику сравнения и установки для GlobalStateCache, используя java.util.concurrent.atomic.AtomicReference.

public class GlobalStateCache {
    AtomicReference<MyImmutableState> atomic = new AtomicReference<MyImmutableState>(MyImmutableState.newInstance(null, null);

    public State getState()
    {
        return atomic.get();
    }

    public void updateState( State currentState, Args newArgs )
    {
        State s = currentState;
        while ( !atomic.compareAndSet( s, MyImmutableState.newInstance( s, newArgs ) ) )
        {
            s = atomic.get();
        }
    }
}

Это, конечно, зависит от затрат на потенциальное создание нескольких дополнительных объектов MyImmutableState и от необходимости повторного запуска myMethod (state), если состояние было обновлено снизу, ноконцепция должна быть правильной.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...