2015-02-05 3 views
11

Пусть есть класс, как это:Java Модель памяти: Смешивание синхронизированное и синхронизируется

public void MyClass { 
    private boolean someoneTouchedMeWhenIWasWorking; 

    public void process() { 
     someoneTouchedMeWhenIWasWorking = false; 
     doStuff(); 
     synchronized(this) { 
      if (someoneTouchedMeWhenIWasWorking) { 
       System.out.println("Hey!"); 
      } 
     } 
    } 

    synchronized public void touch() { 
     someoneTouchedMeWhenIWasWorking = true; 
    } 
} 

Один поток вызывает process, еще звонки touch. Обратите внимание, как process очищает флаг без синхронизации при его запуске.

С моделью памяти Java, возможно ли, что поток, запущенный process, увидит эффект своей локальной, несинхронизированной записи, хотя touch произошло позже?

То есть, если потоки выполняются в следующем порядке:

T1: someoneTouchedMeWhenIWasWorking = false; // Unsynchronized 
T2: someoneTouchedMeWhenIWasWorking = true; // Synchronized 
T1: if (someoneTouchedMeWhenIWasWorking) // Synchronized 

... это вообще возможно, что T1 будет видеть значение из локального кэша? Может ли он сбросить то, что он имеет для памяти в первую очередь (переопределив все, что написал T2), и перезагрузить это значение из памяти?

Необходимо ли синхронизировать первую запись или сделать переменную изменчивой?

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

ответ

1

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

Это хороший вариант использования для изменчивой переменной. «someoneTouchedMeWhenIWasWorking» должен по крайней мере быть переменным.

В вашем примере кода синхронизированный используется для защиты «someoneTouchedMeWhenIWasWorking», который является не чем иным, как флагом. Если он сделан как изменчивый, «синхронизированный» блок/метод можно удалить.

+1

Вопрос не в том, как сделать код безопасным, но безопасен ли он так, как есть? – icza

+0

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

1

Чтобы объяснить, когда значение обновляется, позвольте мне процитировать из этого JSR-133 FAQ:

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

Таким образом, значение someoneTouchedMeWhenIWasWorking публикуется в основную память, когда один поток выходит из блока synchonized. Опубликованное значение затем считывается из основной памяти при входе в блоки synchronized.

За пределами блоков synchronized значение может быть кешировано. Но при входе в синхронизированный блок он будет считываться из основной памяти, и он будет опубликован при выходе из него, даже если он не объявлен volatile.


Существует интересный ответ об этом вопросе здесь: What can force a non-volatile variable to be refreshed?

Вслед за разговор здесь: http://chat.stackoverflow.com/rooms/46867/discussion-between-gray-and-voo

Заметим, что в последнем примере, @SotiriosDelimanolis пытается сделать, чтобы: synchronized(new Object()) {} в то время (! stop), но это не остановилось. Он добавил, что с synchronized(this) он сделал это. Я думаю, что это поведение объясняется в FAQ, когда он заявляет:

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

+0

Я просто попробовал простой пример. Оказывается, фраза ** или во время синхронизированного блока ** очень важна (пропустил ее :() .. Значения читаются из основной памяти даже * в синхронизированном блоке *. JLS является своего рода * дипломатическим * здесь, * в предсказуемом порядке * отличается от * always *. – TheLostMind

+0

Входит ли вход в синхронизированный блок, всегда стирает изменения, сделанные этим потоком, и перезагружает все из основного? –

+1

По-видимому, да: * Прежде чем мы сможем ввести синхронизированный блок, мы получим монитора, что приводит к недействительности локального кэша процессора, так что переменные будут перезагружены из основной памяти. * –

3

Причины JLS с точки зрения происходит, перед тем отношений (Hb). Давайте аннотировать читает и пишет о вашем планируемом исполнения, используя соглашение JLS:

T1: someoneTouchedMeWhenIWasWorking = false; // w 
T2: someoneTouchedMeWhenIWasWorking = true; // w' 
T1: if (someoneTouchedMeWhenIWasWorking)  // r 

Мы имеем следующее происходит, до отношений:

  • Hb (ш, г), из-за правилу заказа программы
  • ро (ш», г), в связи с synchronized keyword (анлок на мониторе происходит, перед тем е очень последующий замок на этом мониторе): если ваша программа выполняет ш « перед тем г (смотрит на часы), которые вы предположила, затем Hb (ш», г)

Ваш вопрос r разрешено читать w. Этот вопрос рассматривается в the last section of #17.4.5:

Мы говорим, что для чтения г с переменной об разрешено наблюдать запись ш к об если в происходит, перед тем частичного порядка следа исполнения:

  • г не заказано до ш (I. е., это не тот случай, когда ро (г, ш)), а
  • нет никакого вмешательства записи ш» к против (т.е. не написать ш» к против таким образом, что ро (ш, ш ') и Hb (ш', г)).

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

  • Первое условие очевидно выполнено: мы определили, что ро (ш, г).
  • Второе условие также верно, потому что нет не происходит, прежде, чем отношения между ш и ш»

Итак, если я не пропустил что-то, кажется, что г теоретически может наблюдать либо w или w '.

На практике, как отмечали другие ответы, типичные JVMs реализованы таким образом, что является более строгим, что JMM, и я не думаю, что любой ток JVM позволит г к наблюдателю ж, но не является гарантией.

В любом случае, проблема достаточно легко исправить, либо с volatile или включением ж в пределах synchronized блока.

+0

Важное различие между * случается до * рассуждения и временного рассуждения состоит в том, что временный порядок является тотальным, а * случается до того, как * является частичным. Поэтому, если вы утверждаете, что * w '* определенно произошло после * w *, вы вводите больше информации, чем то, что существует в терминах *, происходит до *. Поскольку * происходит до *, * w * и * w '* не имеют порядка между ними. –

+0

Однако на практике у вас было бы довольно много проблем с определением того, что временный порядок был в любом реальном исполнении. Тем более, когда вы считаете, что «писать» не является идеализированной точкой времени; это * процесс * происходит в CPU. Операция записи постепенно распространяется через буферы хранения, L1, L2 и т. Д., А другие записи из других ядер выполняют одно и то же одновременно. Кто скажет, кто пришел первым? Вы скоро закончите разговор в очень специфических для процессора терминах, и иллюзия определенного временного порядка испарится. –

+1

... Именно поэтому * происходит до того, как * определяется как это --- в противном случае было бы почти невозможно встретить гарантов JMM, сохраняя при этом максимальную производительность. –

3

С моделью памяти Java, возможно ли, что процесс выполнения потока увидит эффект своей локальной, несинхронизированной записи, хотя касание произошло позже?

Я вижу здесь тонкое, но важное упрощение: как, по вашему мнению, вы определяете, что действие записи вторым потоком произошло точно между действиями записи и чтения первым потоком? Чтобы иметь возможность сказать это, вам нужно будет предположить, что действие записи является идеализированной точкой на временной линии и что все записи происходят в последовательном порядке. На самом аппаратном обеспечении запись представляет собой очень сложный процесс, включающий в себя конвейер процессора, хранилища буферов, кеш L1, кэш второго уровня, шину на передней панели и, наконец, ОЗУ. Такой же процесс происходит одновременно на всех ядрах ЦП. Итак, что именно вы подразумеваете под одной записью «происходит после» другой?

Далее, рассмотрят "Roach Motel" парадигму, которая, кажется, помочь многим людям в качестве своего рода «ментального ярлыка» в последствия модели памяти Java: код легально может быть преобразован в

public void process() { 
    doStuff(); 
    synchronized(this) { 
     someoneTouchedMeWhenIWasWorking = false; 
     if (someoneTouchedMeWhenIWasWorking) { 
      System.out.println("Hey!"); 
     } 
    } 
} 

Этом это совершенно другой подход к использованию свобод, предоставляемых моделью памяти Java, для повышения производительности. Чтение внутри if-предложения действительно потребует считывания фактического адреса памяти, в отличие от прямого вложения значения false и, следовательно, удаления всего if-блока (это действительно произойдет без), но запись false не обязательно пишите в ОЗУ. На следующем этапе рассуждения оптимизирующий компилятор может решить удалить назначение false в целом. В зависимости от особенностей всех других частей кода это одна из вещей, которые могут произойти, но есть много других возможностей.

Главное сообщение, которое нужно убрать из вышеизложенного, должно быть: не притворяйтесь, что можете уклониться от кода Java, используя некоторую упрощенную концепцию «локального кэширования»; вместо этого придерживайтесь официальной модели памяти Java и рассматривайте все доступные ей свободы.

Необходимо ли синхронизировать первую запись или сделать переменную изменчивой?

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

Наконец, представьте, что ваша переменная изменчива. Что это даст вам, что у вас нет сейчас? В терминах формализма JMM две записи будут иметь определенный порядок между ними (наложенный общим порядком синхронизации), но вы будете в том же положении, что и вы сейчас: этот определенный порядок будет произвольным в любых конкретных выполнение.

Смежные вопросы