Видимость данных в многопоточном сценарии
Еще один сценарий, основанный на предыдущем вопросе. На мой взгляд, его заключение будет достаточно общим, чтобы быть полезным для широкой аудитории. Цитирую Питера Лоури отсюда:
Синхронизированный использует барьер памяти, который гарантирует, что ВСЕ память находится в согласованном состоянии для этого потока, независимо от того, указана ли она в блоке или нет.
Прежде всего, моя проблема связана только с видимостью данных. То есть атомарность ("синхронизация операций") уже гарантирована в моем программном обеспечении, поэтому каждая операция записи завершается перед любой операцией чтения с тем же значением, и наоборот, и так далее. Таким образом, вопрос только о потенциально кэшированных значениях по потокам.
Рассмотрим 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()
вызывается threadBoperationB()
это единственный метод, вызываемый 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()
, То есть теперь все безопасно. С другой стороны, логический порядок вызовов методов следующий:
operationA1()
operationB()
dummyA()
operationA2()
Мой вывод: из-за синхронизированного блока в operationB()
, threadB увидит самые последние значения данных, которые могли быть изменены ранее (например, в operationA1()
). Из-за синхронизированного блока в dummyA()
, threadA увидит самые последние копии данных, которые были изменены в operationB()
, Есть ли какая-то ошибка в этом ходу мыслей?
1 ответ
Ваша собственная интуиция в отношении вопроса 2 в целом верна. Использование synchronized(mLock) в начале операции A2 создаст барьер памяти, который обеспечит, чтобы при последующих чтениях с помощью операции A2 были видны записи, выполненные операцией B, которые были опубликованы из-за барьера памяти, неявного с использованием синхронизированного (млок) в работе.
Однако, чтобы ответить на вопрос 1, обратите внимание, что операция B может не видеть никаких операций записи, выполняемых операцией A1, если вы не вставите полный барьер памяти в конце операции A1 (т. Е. Нет ничего, что говорило бы системе сбрасывать значения из кэша потока операции A1). Поэтому вы можете захотеть сделать вызов dummyA в конце операции A1.
Чтобы быть полностью безопасным и более обслуживаемым, и поскольку вы заявляете, что выполнение этих методов не перекрывают друг друга, вы должны заключить все манипуляции с общим состоянием в синхронизированный (mLock) блок без потери производительности.