Видимость данных в многопоточном сценарии

Еще один сценарий, основанный на предыдущем вопросе. На мой взгляд, его заключение будет достаточно общим, чтобы быть полезным для широкой аудитории. Цитируя Питера Лоури из здесь:

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

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

Рассмотрим 2 потока, threadA и threadB, и следующий класс:

public class SomeClass {

private final Object mLock = new Object();    
// Note: none of the member variables are volatile.

public void operationA1() {
   ... // do "ordinary" stuff with the data and methods of SomeClass

     /* "ordinary" stuff means we don't create new Threads,
         we don't perform synchronizations, create semaphores etc.
     */
}

public void operationB() {
  synchronized(mLock) {
     ...
     // do "ordinary" stuff with the data and methods of SomeClass
  }
}

// public void dummyA() {
// synchronized(mLock) {
//    dummyOperation();
//  }
// }

public void operationA2() {
   // dummyA();  // this call is commented out

   ... // do "ordinary" stuff with the data and methods of SomeClass
}
}

Известные факты (вытекают из архитектуры моего софта):

  • operationA1() и operationA2() вызываются threadA, operationB() вызывается threadB
  • operationB() — это единственный метод, вызываемый threadB в этом классе. Обратите внимание, что operationB() находится в синхронизированном блоке.
  • очень важно: гарантируется, что эти операции вызываются в следующем логическом порядке: operationA1(), operationB(), operationA2(). Гарантируется, что каждая операция завершается до вызова предыдущей. Это связано с архитектурной синхронизацией более высокого уровня (очередь сообщений, но сейчас это не имеет значения). Как я уже сказал, мой вопрос связан исключительно с видимостью данных (то есть являются ли копии данных актуальными или устаревшими, например, из-за собственного кеша потока).

Согласно цитате Питера Лоури, барьер памяти в operationB() гарантирует, что вся память будет находиться в согласованном состоянии для threadB в течение operationB(). Поэтому, например. если threadA изменил некоторые значения в operationA1(), эти значения будут записаны в основную память из кэша threadA к моменту запуска operationB(). Вопрос 1. Это правильно?

Вопрос №2: когда operationB() покидает барьер памяти, значения, измененные operationB() (и, возможно, кэшированные threadB), будут записаны обратно в основную память. Но операция A2() не будет безопасной, потому что никто не просил threadA синхронизироваться с основной памятью, верно? Таким образом, не имеет значения, что изменения operationB() теперь находятся в основной памяти, потому что threadA все еще может иметь кэшированные копии со времени, предшествующего вызову operationB().

Вопрос №3: если мое подозрение в вопросе №2 верно, то снова проверьте мой исходный код и раскомментируйте метод dummyA(), а также раскомментируйте вызов dummyA() в operationA2(). Я знаю, что это может быть плохой практикой в ​​других отношениях, но имеет ли это значение? Мое (возможно ошибочное) предположение таково: dummyA() заставит threadA обновить кэшированные данные из основной памяти (из-за блока mLock synchronized), поэтому он увидит все изменения, сделанные operationB(). То есть теперь все в безопасности. Кстати, логический порядок вызовов методов следующий:

  1. operationA1()
  2. operationB()
  3. dummyA()
  4. operationA2()

Мой вывод: из-за синхронизированного блока в operationB() threadB увидит самые актуальные значения данных, которые могли быть изменены ранее (например, в operationA1()). Из-за синхронизированного блока в dummyA() threadA увидит самые последние копии данных, которые были изменены в operationB(). Есть ли ошибка в этом ходе мыслей?


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


Ответы (1)


Ваша собственная интуиция относительно вопроса 2 в целом верна. Использование synchronized(mLock) в начале операции A2 создаст барьер памяти, который гарантирует, что дальнейшие операции чтения операцией A2 будут видеть записи, выполненные операцией B, которые были опубликованы из-за барьера памяти, неявного с использованием synchronized( mlock) в работеB.

Однако, чтобы ответить на вопрос 1, обратите внимание, что операция B может не увидеть записи, выполненные операцией A1, если вы не вставите полный барьер памяти в конце операции A1 (т. е. ничто не говорит системе сбрасывать значения из кэша потока операции A1). Таким образом, вы можете захотеть поместить вызов dummyA в конце операции A1.

Чтобы быть полностью безопасным и более удобным в сопровождении, а также поскольку вы заявляете, что выполнение этих методов не перекрывает друг друга, вы должны заключить все манипуляции с общим состоянием в блок synchronized(mLock) без потери производительности.

person Monroe Thomas    schedule 03.07.2012