Что означает сброс локальной памяти в глобальную память?

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

В этом контексте я никогда полностью не понимал ссылки на "поточно-локальную" память. Я понимаю, что данные, которые существуют только в стеке, являются поточно-локальными, но когда речь идет об объектах в куче, мое понимание становится туманным.

Я надеялся получить комментарии по следующим пунктам:

  • При выполнении на компьютере с несколькими процессорами очистка потоковой локальной памяти просто относится к очистке кэша процессора в ОЗУ?

  • При выполнении на однопроцессорной машине это означает что-нибудь вообще?

  • Если куча может иметь одну и ту же переменную в двух разных ячейках памяти (каждый из них обращается к другому потоку), при каких обстоятельствах это может возникнуть? Каковы последствия этого для сбора мусора? Как настойчиво делают виртуальные машины такого рода?

  • (EDIT: добавление вопроса 4) Какие данные удаляются при выходе из синхронизированного блока? Это все, что поток имеет локально? Это только записи, которые были сделаны внутри синхронизированного блока?

    Object x = goGetXFromHeap(); // x.f is 1 here    
    Object y = goGetYFromHeap(); // y.f is 11 here
    Object z = goGetZFromHead(); // z.f is 111 here
    
    y.f = 12;
    
    synchronized(x)
    {
        x.f = 2;
        z.f = 112;
    }
    
    // will only x be flushed on exit of the block? 
    // will the update to y get flushed?
    // will the update to z get flushed?
    

В целом, я думаю, я пытаюсь понять, является ли поточно-локальная память физически доступной только одному процессору или если существует логическое разбиение на кучи на основе нитей, выполняемое виртуальной машиной?

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

Большое спасибо.

Ответ 1

Флеш, о котором вы говорите, известен как "барьер памяти". Это означает, что ЦП гарантирует, что то, что он видит в ОЗУ, также можно просматривать с других ЦП/ядер. Это подразумевает две вещи:

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

  • Реализация synchronized также выполняет барьер памяти, чтобы убедиться, что все изменения в ОЗУ из текущего ядра распространяются на основную ОЗУ (или, по крайней мере, все остальные ядра знают, что это ядро ​​имеет последнее значения - протоколы когерентности кеша могут быть довольно сложными).

Второе задание тривиально в однопроцессорных системах (я имею в виду системы с одним процессором, имеющим одно ядро), но однопроцессорные системы в настоящее время становятся все реже.

Что касается локальных кучей потоков, это теоретически можно сделать, но обычно это не стоит усилий, потому что ничего не говорит о том, какие части памяти должны быть сброшены с помощью synchronized. Это ограничение модели thread-with-shared-memory: предполагается, что вся память будет разделяться. При первом встреченном synchronized, JVM должен затем очистить все свои "объекты локальной кучи" до основной ОЗУ.

Однако недавняя JVM от Sun может выполнить "анализ побега", в котором JVM преуспевает в доказательстве того, что некоторые экземпляры никогда не становятся видимыми из других потоков. Это типично для экземпляров StringBuilder, созданных javac для обработки конкатенации строк. Если экземпляр никогда не передается как параметр другим методам, он не становится "глобально видимым". Это делает его подходящим для распределения потоков по локальной сети или даже при правильных обстоятельствах для распределения на основе стека. Обратите внимание, что в этой ситуации дублирование отсутствует; экземпляр не находится в "двух местах одновременно". Только JVM может сохранить экземпляр в частном месте, которое не несет затрат на барьер памяти.

Ответ 2

Это действительно деталь реализации, если текущее содержимое памяти объекта, которое не синхронизировано, видимо для другого потока.

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

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

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

Для большего чтения я рекомендую Java Concurrency на практике. Это действительно большая практическая книга по этому вопросу.

Ответ 3

Это не так просто, как CPU-Cache-RAM. Это все завернуто в JVM и JIT, и они добавляют свое собственное поведение.

Взгляните на Декларация с двойной проверкой блокировки. Это трактат о том, почему блокировка с двойной проверкой не работает, но также объясняет некоторые нюансы модели памяти Java.