Могут ли несколько потоков видеть записи в ByteBuffer с прямым отображением в Java?

Я работаю над тем, что использует ByteBuffers, построенные из отображенных в память файлов (через FileChannel.map ()), а также прямые ByteBuffers в памяти. Я пытаюсь понять ограничения модели параллелизма и памяти.

Я прочитал все соответствующие Javadoc (и исходный код) для таких вещей, как FileChannel, ByteBuffer, MappedByteBuffer и т. Д. Кажется очевидным, что определенный ByteBuffer (и соответствующие подклассы) имеет несколько полей, и состояние не защищено от модели памяти точка зрения. Таким образом, вы должны синхронизироваться при изменении состояния определенного ByteBuffer, если этот буфер используется в потоках. Обычные приемы включают использование ThreadLocal для обертывания ByteBuffer, дублирование (при синхронизации) для получения нового экземпляра, указывающего на те же отображенные байты, и т. Д.

Учитывая этот сценарий:

  1. менеджер имеет отображенный байтовый буфер B_all для всего файла (скажем, это <2 ГБ)
  2. менеджер вызывает duplicate(), position(), limit() и slice() для B_all, чтобы создать новый меньший ByteBuffer B_1 что кусок файла и дает это потоку T1
  3. менеджер делает все то же самое, чтобы создать ByteBuffer B_2 указывая на те же отображенные байты и дает это потоку T2

Мой вопрос: может ли T1 писать в B_1, а T2 писать в B_2 одновременно и гарантированно видеть изменения друг друга? Может ли T3 использовать B_all для чтения этих байтов и гарантированно увидеть изменения как от T1, так и от T2?

Мне известно, что записи в сопоставленный файл не обязательно будут отображаться в разных процессах, если только вы не используете force() для указания ОС записывать страницы на диск. Меня это не волнует. Предположим для этого вопроса, что эта JVM является единственным процессом, пишущим один сопоставленный файл.

Примечание: я не ищу догадки (я могу сделать это довольно хорошо сам). Я хотел бы, чтобы ссылки на что-то определенное о том, что (или нет) гарантируется для отображаемых в памяти прямых буферов. Или, если у вас есть реальный опыт или отрицательные тестовые случаи, это также может служить достаточным доказательством.

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

7 ответов

Отображение памяти с помощью JVM - это просто тонкая оболочка для CreateFileMapping (Windows) или mmap (posix). Таким образом, у вас есть прямой доступ к буферному кешу ОС. Это означает, что эти буферы - это то, что ОС считает файлом содержать (и ОС в конечном итоге синхронизирует файл, чтобы отразить это).

Таким образом, нет необходимости вызывать force() для синхронизации между процессами. Процессы уже синхронизированы (через ОС - даже чтение / запись обращается к одним и тем же страницам). Принудительная синхронизация между ОС и контроллером накопителя (между контроллером накопителя и физическими дисками может быть некоторая задержка, но у вас нет аппаратной поддержки, чтобы что-то с этим сделать).

Независимо от того, отображенные в память файлы являются приемлемой формой разделяемой памяти между потоками и / или процессами. Единственная разница между этой совместно используемой памятью и, скажем, именованным блоком виртуальной памяти в Windows - это возможная синхронизация с диском (фактически, mmap делает виртуальную память без файла путем отображения /dev/null).

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

Для получения дополнительной информации посмотрите mmap в posix (или CreateFileMapping для Windows, который был построен почти таким же образом.

Нет. Модель памяти JVM (JMM) не гарантирует, что несколько потоков, изменяющих (несинхронизированные) данные, увидят изменения друг друга.

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

Давайте перефразируем вопрос так, чтобы он о байтовых массивах:

  1. У менеджера есть байтовый массив: byte[] B_all
  2. Создана новая ссылка на этот массив: byte[] B_1 = B_allи передается в поток T1
  3. Еще одна ссылка на этот массив создан: byte[] B_2 = B_allи передается в поток T2

До пишет в B_1 по нитке T1 увидеть в B_2 по нитке T2?

Нет, такие записи не гарантируются, без какой-либо явной синхронизации между T_1 а также T_2, Суть проблемы в том, что JIT JVM, процессор и архитектура памяти могут свободно изменять порядок доступа к памяти (не только для того, чтобы разозлить вас, но и для повышения производительности за счет кэширования). Все эти уровни ожидают, что программное обеспечение будет явным (через блокировки, изменчивые или другие явные подсказки) о том, где требуется синхронизация, подразумевая, что эти слои могут свободно перемещать объекты, когда такие подсказки не предоставляются.

Обратите внимание, что на практике то, видите ли вы записи или нет, зависит в основном от аппаратного обеспечения и выравнивания данных на разных уровнях кэшей и регистров, а также от того, насколько "далеко" запущенные потоки находятся в иерархии памяти.

JSR-133 была попыткой точно определить модель памяти Java около Java 5.0 (и насколько я знаю, она все еще применима в 2012 году). Вот где вы хотите найти окончательные (хотя и плотные) ответы: http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf (наиболее актуален раздел 2). Более читаемые материалы можно найти на веб-странице JMM: http://www.cs.umd.edu/~pugh/java/memoryModel/

Часть моего ответа утверждает, что ByteBuffer ничем не отличается от byte[] с точки зрения синхронизации данных. Я не могу найти конкретную документацию, которая говорит об этом, но я предлагаю, чтобы в разделе "Безопасность потоков" в документе java.nio.Buffer упоминалось что-то о синхронизации или энергозависимости, если это применимо. Поскольку в документе об этом не упоминается, мы не должны ожидать такого поведения.

Самое дешевое, что вы можете сделать, это использовать переменную volatile. После того, как поток записывает в отображенную область, он должен записать значение в переменную volatile. Любой поток чтения должен прочитать переменную volatile перед чтением сопоставленного буфера. Выполнение этого приводит к "случаю раньше" в модели памяти Java.

Обратите внимание, что у вас НЕТ гарантии, что другой процесс находится в процессе написания чего-то нового. Но если вы хотите гарантировать, что другие потоки могут видеть то, что вы написали, запись volatile (с последующим чтением из потока чтения) поможет.

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

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

Простой способ изменить данные между потоками - использовать обменник

Исходя из примера,

class FillAndEmpty {
   final Exchanger<ByteBuffer> exchanger = new Exchanger<ByteBuffer>();
   ByteBuffer initialEmptyBuffer = ... a made-up type
   ByteBuffer initialFullBuffer = ...

   class FillingLoop implements Runnable {
     public void run() {
       ByteBuffer currentBuffer = initialEmptyBuffer;
       try {
         while (currentBuffer != null) {
           addToBuffer(currentBuffer);
           if (currentBuffer.remaining() == 0)
             currentBuffer = exchanger.exchange(currentBuffer);
         }
       } catch (InterruptedException ex) { ... handle ... }
     }
   }

   class EmptyingLoop implements Runnable {
     public void run() {
       ByteBuffer currentBuffer = initialFullBuffer;
       try {
         while (currentBuffer != null) {
           takeFromBuffer(currentBuffer);
           if (currentBuffer.remaining() == 0)
             currentBuffer = exchanger.exchange(currentBuffer);
         }
       } catch (InterruptedException ex) { ... handle ...}
     }
   }

   void start() {
     new Thread(new FillingLoop()).start();
     new Thread(new EmptyingLoop()).start();
   }
 }

Один из возможных ответов, с которыми я столкнулся, - использование блокировок файлов для получения монопольного доступа к части диска, отображаемой буфером. Это объясняется на примере здесь, например.

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

Конечно, я бы хотел избежать блокировки файлов или синхронизации страниц, если jvm / os гарантирует согласованность.

Я не думаю, что это гарантировано. Если модель памяти Java не говорит, что она гарантирована, это по определению не гарантируется. Я бы либо охранял запись в буфер с синхронизированными, либо запись в очередь для одного потока, который обрабатывает все записи. Последний прекрасно работает с многоядерным кешированием (лучше иметь по 1 записывающему устройству на каждую ячейку памяти).

Нет, он ничем не отличается от обычных переменных Java или элементов массива.

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