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

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

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

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

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

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

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

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

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

Ответ 1

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

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

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

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

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

Ответ 2

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

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

Позвольте перефразировать вопрос так, чтобы его байтовые массивы:

  • Менеджер имеет массив байтов: byte[] B_all
  • Создана новая ссылка на этот массив: byte[] B_1 = B_all и задан для потока T1
  • Создана другая ссылка на этот массив: byte[] B_2 = B_all и задан для потока T2

Делает ли запись в B_1 потоком T1 видным в B_2 потоком T2?

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

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

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/

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

Ответ 3

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

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

Ответ 4

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

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

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

В соответствии с примером,

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();
   }
 }

Ответ 5

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

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

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

Ответ 6

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

Ответ 7

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