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

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

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

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

Рассмотрим 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(), Поэтому, например, если поток А изменил некоторые значения в operationA1() эти значения будут записаны в основную память из кеша threadA к тому времени operationB() запущен Вопрос № 1: это правильно?

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

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

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

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

1 ответ

Решение

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

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

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

Другие вопросы по тегам