Многопоточный доступ и переменный кеш потоков

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

Предположим, есть этот класс:

public class TestClass {
   private int someValue;

   public int getSomeValue() { return someValue; }
   public void setSomeValue(int value) {  someValue = value; }
}

Есть два потока (A и B), которые обращаются к экземпляру этого класса. Рассмотрим следующую последовательность:

  1. О: getSomeValue ()
  2. B: setSomeValue ()
  3. О: getSomeValue ()

Если я прав, someValue должен быть изменчивым, иначе третий шаг может не вернуть актуальное значение (потому что A может иметь кешированное значение). Это правильно?

Второй сценарий:

  1. B: setSomeValue ()
  2. О: getSomeValue ()

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

Если доступ к классу осуществляется только вторым способом, нет необходимости в энергозависимости / синхронизации, или это так?

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


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

  1. Из потока пользовательского интерфейса: threadA.start()
  2. threadA вызывает getSomeValue() и сообщает потоку пользовательского интерфейса
  3. Поток пользовательского интерфейса получает сообщение (в своей очереди сообщений), поэтому он вызывает: threadB.start()
  4. threadB вызывает setSomeValue() и сообщает потоку пользовательского интерфейса
  5. Поток пользовательского интерфейса получает сообщение и информирует threadA (каким-то образом, например, очередь сообщений)
  6. threadA вызывает getSomeValue()

Это полностью синхронизированная структура, но почему это означает, что threadA получит самое актуальное значение на шаге 6? (если someValue не является изменчивым или не помещается в монитор при доступе из любого места)


person Thomas Calc    schedule 29.06.2012    source источник


Ответы (5)


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

В этом случае «кэширование» обычно относится к активности, которая происходит на аппаратном уровне. В java происходят определенные события, которые заставляют ядра многопроцессорных систем «синхронизировать» свои кэши. Доступ к изменчивым переменным - пример этого, синхронизированные блоки - другой. Представьте себе сценарий, в котором эти два потока X и Y запланированы для работы на разных процессорах.

X starts and is scheduled on proc 1
y starts and is scheduled on proc 2

.. now you have two threads executing simultaneously
to speed things up the processors check local caches
before going to main memory because its expensive.

x calls setSomeValue('x-value') //assuming proc 1's cache is empty the cache is set
                                //this value is dropped on the bus to be flushed
                                //to main memory
                                //now all get's will retrieve from cache instead
                                //of engaging the memory bus to go to main memory 
y calls setSomeValue('y-value') //same thing happens for proc 2

//Now in this situation depending on to order in which things are scheduled and
//what thread you are calling from calls to getSomeValue() may return 'x-value' or
//'y-value. The results are completely unpredictable.  

Дело в том, что volatile (в совместимых реализациях) гарантирует, что упорядоченные записи всегда будут сбрасываться в основную память и что кеши других процессоров будут помечены как «грязные» перед следующим доступом независимо от потока, из которого происходит этот доступ.

отказ от ответственности: volatile НЕ БЛОКИРУЕТСЯ. Это особенно важно в следующих случаях:

volatile int counter;

public incrementSomeValue(){
    counter++; // Bad thread juju - this is at least three instructions 
               // read - increment - write             
               // there is no guarantee that this operation is atomic
}

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

Если намерение состоит в том, чтобы getSomeValue() всегда отражал самый последний вызов setSomeValue(), то это хорошее место для использования ключевого слова volatile. Просто помните, что без него нет гарантии, что getSomeValue() отразится на самом последнем вызове setSomeValue(), даже если setSomeValue() был запланирован первым.

person nsfyn55    schedule 29.06.2012
comment
Поэтому, если я хочу быть в безопасности в любой среде (очевидно, это обязательно), то многопоточный доступ к переменным должен быть синхронизирован независимо от порядка операций (т.е. независимо от того, что сами операции синхронизируются)? Т.е. даже если порядок операций строго следует из моей программной структуры (как в примере в конце моего сообщения), все равно должна быть синхронизация более низкого уровня (на самом деле это не синхронизация, а способ гарантировать, что копии переменных обновляются там, где это необходимо - в Java этому требованию соответствует и соответствующий синхронизированный блок). - person Thomas Calc; 30.06.2012
comment
Короче говоря, доступ к переменным, к которым осуществляется доступ из большего числа потоков (даже если потоки ждут завершения операций записи друг друга, как упомянул Роберт Харви), следует осуществлять таким образом, чтобы система обновлялась. любые кешированные копии. Это правильно? - person Thomas Calc; 30.06.2012
comment
это действительно зависит от контекста. Вообще говоря, доступ к общему состоянию должен быть синхронизирован, за исключением нескольких угловых случаев. Одним из ярких примеров сбоя является вход в цикл, когда обе стороны для выхода полагаются на обновления общей переменной. это может привести к непреднамеренному бесконечному циклу в многоядерных системах, где каждый поток выполняет цикл в своей собственной копии. Опытные параллельные разработчики идут на экстраординарные меры (защитные копии, неизменяемые объекты, квазифункциональное программирование и т. Д.), Чтобы вообще не иметь общего состояния, чтобы избежать необходимости синхронизации. - person nsfyn55; 30.06.2012

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

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

person Robert Harvey    schedule 29.06.2012
comment
Если два потока вызывают одни и те же методы, вы не можете дать никаких гарантий относительно порядка, в котором вызываются указанные методы. - в моих случаях использования есть гарантия. Это четко определенный процесс. в моем программном обеспечении потоки используют синхронизацию. Итак, в моем конкретном контексте гарантированный порядок операций следует из структуры программного обеспечения. Вопрос в том, будут ли эти (правильно упорядоченные) операции видеть правильное значение без с использованием мониторов / volatile. (Тривиальный пример: поток A не запущен, когда поток B вызывает getSomeValue (), например, поток A является начался именно тогда, когда вызывается getSomeValue ().) - person Thomas Calc; 30.06.2012
comment
Я слышу, что вы говорите, но я все еще утверждаю, что порядок действий не имеет значения; важно то, что второй поток ожидает, пока первый поток завершит свою операцию записи. Если ваш процесс это гарантирует, то, думаю, его можно считать потокобезопасным. - person Robert Harvey; 30.06.2012
comment
Другими словами, если вы уже используете механизмы синхронизации, чтобы гарантировать безопасность потоков (как вы говорите), вам не нужно думать о гарантии порядка операций. - person Robert Harvey; 30.06.2012
comment
Я гарантирую порядок операций, да, но (без использования volatile) в первом сценарии, как поток A может узнать, что значение изменилось с момента последнего обращения к нему? Я имею в виду, что само программное обеспечение знает о шаге 2, но поток A может не знать. - person Thomas Calc; 30.06.2012
comment
Я добавил еще один пример в конец основного вопроса (чтобы обеспечить достойное форматирование). - person Thomas Calc; 30.06.2012

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

У меня был этот пример, где используется volatile, а затем я использовал 2 потока, которые увеличивали значение счетчика на 1 каждый раз до 10000. Таким образом, это должно быть в общей сложности 20000. Но, к моему удивлению, это происходило не всегда.

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

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

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

person Kumar Vivek Mitra    schedule 30.06.2012

Давайте посмотрим на книгу.

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

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

Также см. эту предыдущую статью, в которой объясняется что int доступ обязательно атомарный (но не double или long).

Вместе они означают, что если ваше int поле объявлено volatile, то никакие блокировки не требуются, чтобы гарантировать атомарность: вы всегда будете видеть значение, которое было последним записано в ячейку памяти, а не какое-то запутанное значение, полученное в результате наполовину полной записи (насколько это возможно с двойным или длинным).

Однако вы, кажется, подразумеваете, что ваши геттеры и сеттеры являются атомарными. Это не гарантируется. JVM может прерывать выполнение в промежуточных точках во время вызова или возврата. В этом примере это не имеет последствий. Но если у звонков были побочные эффекты, например setSomeValue(++val), тогда у вас была бы другая история.

person Gene    schedule 30.06.2012

Если я прав, someValue должен быть изменчивым, иначе третий шаг может не вернуть актуальное значение (потому что A может иметь кешированное значение). Это правильно?

Если поток B вызывает setSomeValue (), вам нужна какая-то синхронизация, чтобы гарантировать, что поток A может прочитать это значение. volatile не выполнит этого самостоятельно, как и синхронизация методов. Код, который делает это, в конечном итоге является тем кодом синхронизации, который вы добавили, чтобы убедиться, что A: getSomeValue() произойдет после B: setSomeValue(). Если, как вы предлагаете, вы использовали очередь сообщений для синхронизации потоков, это происходит потому, что изменения памяти, сделанные потоком A, стали видимыми для потока B, как только поток B установил блокировку вашей очереди сообщений.

Если доступ к классу осуществляется только вторым способом, нет необходимости в энергозависимости / синхронизации, или это так?

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

Если у вас есть класс, который вы хотите обновлять и читать из нескольких потоков, я бы, вероятно, сделал самое простое для запуска, а именно синхронизацию всех общедоступных методов. Если вы действительно считаете, что это узкое место, вы можете изучить некоторые из более сложных механизмов блокировки в Java.

Так что же гарантирует нестабильность?

Для точной семантики вам, возможно, придется прочитать учебные пособия, но один из способов резюмировать это так: 1) любые изменения памяти, сделанные последним потоком для доступа к volatile, будут видны текущему потоку, обращающемуся к volatile, и 2) что доступ к volatile является атомарным (это не будет частично созданный объект, частично присвоенный двойной или длинный).

Синхронизированные блоки имеют аналогичные свойства: 1) любые изменения памяти, сделанные последним потоком для доступа к блокировке, будут видны этому потоку, и 2) изменения, сделанные внутри блока, выполняются атомарно по отношению к другим синхронизированным блокам.

(1) означает любые изменения памяти, а не только изменения в энергозависимой (мы говорим о публикации JDK 1.5) или в синхронизированном блоке. Это то, что люди имеют в виду, когда говорят о порядке, и это достигается по-разному на разных архитектурах микросхем, часто с помощью барьеров памяти.

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

person nas    schedule 30.06.2012