В предыдущих ответах не подчеркивается самая полезная вещь о пустых блоках synchronized
: они могут обеспечить видимость изменений переменных и других действий в потоках. Как указывает jtahlborn , синхронизация создает «барьер памяти» для компилятора, который заставляет его очищать и обновлять кэши. Но я не нашел, где «SnakE обсуждает» это, поэтому я сам написал ответ.
int variable;
void test() // This code is INCORRECT
{
new Thread( () -> // A
{
variable = 9;
for( ;; )
{
// Do other stuff
}
}).start();
new Thread( () -> // B
{
for( ;; )
{
if( variable == 9 ) System.exit( 0 );
}
}).start();
}
Указанная выше программа неверна. Значение переменной может кэшироваться локально в потоке A или B или в обоих. Таким образом, B может никогда не прочитать значение 9, которое записывает A, и поэтому может зацикливаться вечно.
Сделать изменение переменной видимым в потоках с помощью пустых synchronized
блоков
Одним из возможных исправлений является добавление к переменной модификатора volatile
(фактически «без кеша»). Однако иногда это неэффективно, поскольку полностью запрещает кэширование переменной. Пустые блоки synchronized
, с другой стороны, не запрещают кэширование. Все, что они делают, это заставляют кэши синхронизироваться с основной памятью в определенных критических точках. Например: *
int variable;
void test() // Corrected version
{
new Thread( () -> // A
{
variable = 9;
synchronized( o ) {} // Flush to main memory
for( ;; )
{
// Do other stuff
}
}).start();
new Thread( () -> // B
{
for( ;; )
{
synchronized( o ) {} // Refresh from main memory
if( variable == 9 ) System.exit( 0 );
}
}).start();
}
final Object o = new Object();
Как модель памяти гарантирует видимость
Оба потока должны синхронизироваться на одном и том же объекте, чтобы гарантировать видимость. Эта гарантия опирается на модель памяти Java , в частности на правило, что «действие разблокировки на мониторе m синхронизируется с всеми последующими действиями блокировки на m», и, таким образом, происходит -перед этими действиями. Таким образом, А разблокирует монитор О в хвосте своего блока synchronized
, прежде чем последующая блокировка В в начале его блока. (Обратите внимание, что этот странный порядок следования связей объясняет, почему тела могут быть пустыми.) Учитывая также, что запись A предшествует разблокировке, а блокировка B предшествует чтению, отношение должно расширяться, чтобы охватить как запись, так и чтение: запись происходит, прежде чем читать . Именно это важное, расширенное отношение делает пересмотренную программу корректной с точки зрения модели памяти.
Я думаю, что это самое важное использование для пустых synchronized
блоков.
* Я говорю так, как будто речь идет о кешировании процессора
потому что я думаю, что это полезный способ просмотра.
По правде говоря, как прокомментировал Александр Дубинский, «все современные процессоры согласованы с кэшем.
Отношение «происходит до» больше связано с тем, что разрешено делать компилятору, а не с процессором. ’