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

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

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

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

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

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

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

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

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

Ответы [ 7 ]

16 голосов
/ 11 августа 2011

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

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

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

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

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

4 голосов
/ 19 апреля 2012

Нет. Модель памяти 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[] в плане синхронизации данных. Я не могу найти конкретную документацию, которая говорит об этом, но я предлагаю, чтобы в разделе «Thread Safety» документа java.nio.Buffer упоминалось что-то о синхронизации или энергозависимости, если это применимо. Поскольку в документе об этом не упоминается, мы не должны ожидать такого поведения.

3 голосов
/ 17 февраля 2012

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

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

1 голос
/ 10 августа 2011

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

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

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

1 голос
/ 10 августа 2011

Я бы предположил, что прямая память обеспечивает те же гарантии или их отсутствие, что и кучная память. Если вы измените 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();
   }
 }
0 голосов
/ 10 августа 2011

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

0 голосов
/ 10 августа 2011

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

...