Перейти к основному содержимому

Как заблокировать файл в Java

· 7 мин. чтения

1. Обзор

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

В этом руководстве мы рассмотрим различные подходы к достижению этой цели с помощью библиотеки Java NIO .

2. Введение в блокировки файлов

В общем, есть два типа замков :

  • Эксклюзивные блокировки — также известные как блокировки записи.

  • Общие блокировки — также называемые блокировками чтения.

Проще говоря, монопольная блокировка предотвращает все другие операции, включая чтение, пока завершается операция записи.

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

В следующем разделе мы увидим, как Java обрабатывает эти типы блокировок.

3. Блокировка файлов в Java

Библиотека Java NIO позволяет блокировать файлы на уровне ОС. Для этой цели предназначены методы lock() и tryLock() FileChannel .

Мы можем создать FileChannel через FileInputStream , FileOutputStream или RandomAccessFile . Все три имеют метод getChannel() , который возвращает FileChannel .

В качестве альтернативы мы можем создать FileChannel напрямую через статический метод открытия :

try (FileChannel channel = FileChannel.open(path, openOptions)) {
// write to the channel
}

Далее мы рассмотрим различные варианты получения эксклюзивных и общих блокировок в Java. Чтобы узнать больше о файловых каналах, ознакомьтесь с нашим руководством по Java FileChannel .

4. Эксклюзивные замки

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

Мы получаем эксклюзивные блокировки, вызывая lock() или tryLock() в классе FileChannel . Мы также можем использовать их перегруженные методы:

  • блокировка (длинная позиция, длинный размер, логическое совместное использование)
  • tryLock(длинная позиция, длинный размер, логический разделяемый)

В этих случаях для общего параметра должно быть установлено значение false .

Чтобы получить эксклюзивную блокировку, мы должны использовать доступный для записи FileChannel . Мы можем создать его с помощью методов getChannel() FileOutputStream или RandomAccessFile . В качестве альтернативы, как упоминалось ранее, мы можем использовать статический метод открытия класса FileChannel . Все, что нам нужно, это установить для второго аргумента значение StandardOpenOption.APPEND :

try (FileChannel channel = FileChannel.open(path, StandardOpenOption.APPEND)) { 
// write to channel
}

4.1. Эксклюзивные блокировки с использованием FileOutputStream

FileChannel , созданный из FileOutputStream , доступен для записи. Таким образом, мы можем получить эксклюзивную блокировку:

try (FileOutputStream fileOutputStream = new FileOutputStream("/tmp/testfile.txt");
FileChannel channel = fileOutputStream.getChannel();
FileLock lock = channel.lock()) {
// write to the channel
}

Здесь channel.lock() будет либо блокироваться до тех пор, пока не получит блокировку, либо выдаст исключение. Например, если указанная область уже заблокирована, создается исключение OverlappingFileLockException . Полный список возможных исключений см. в Javadoc .

Мы также можем выполнить неблокирующую блокировку, используя channel.tryLock() . Если не удается получить блокировку, потому что другая программа удерживает перекрывающуюся блокировку, она возвращает null . Если это не удается сделать по какой-либо другой причине, генерируется соответствующее исключение.

4.2. Эксклюзивные блокировки с использованием RandomAccessFile

С RandomAccessFile нам нужно установить флаги для второго параметра конструктора .

Здесь мы собираемся открыть файл с разрешениями на чтение и запись:

try (RandomAccessFile file = new RandomAccessFile("/tmp/testfile.txt", "rw");
FileChannel channel = file.getChannel();
FileLock lock = channel.lock()) {
// write to the channel
}

Если мы откроем файл в режиме только для чтения и попытаемся записать в его канал, он выдаст исключение NonWritableChannelException .

4.3. Эксклюзивные блокировки требуют доступного для записи FileChannel

Как упоминалось ранее, монопольным блокировкам нужен доступный для записи канал. Следовательно, мы не можем получить эксклюзивную блокировку через FileChannel , созданный из FileInputStream :

Path path = Files.createTempFile("foo","txt");
Logger log = LoggerFactory.getLogger(this.getClass());
try (FileInputStream fis = new FileInputStream(path.toFile());
FileLock lock = fis.getChannel().lock()) {
// unreachable code
} catch (NonWritableChannelException e) {
// handle exception
}

В приведенном выше примере метод lock() вызовет исключение NonWritableChannelException . Действительно, это потому, что мы вызываем getChannel для FileInputStream , который создает канал только для чтения.

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

5. Общие замки

Помните, что общие блокировки также называются блокировками чтения . Следовательно, чтобы получить блокировку чтения, мы должны использовать читаемый FileChannel .

Такой FileChannel можно получить, вызвав метод getChannel() для FileInputStream или RandomAccessFile . Опять же, другой вариант — использовать статический метод открытия класса FileChannel . В этом случае мы устанавливаем второй аргумент в StandardOpenOption.READ :

try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
// read from the channel
}

Здесь следует отметить, что мы решили заблокировать весь файл, вызвав lock(0, Long.MAX_VALUE, true) . Мы также могли бы заблокировать только определенную область файла, изменив первые два параметра на разные значения. Третий параметр должен быть установлен в true в случае общей блокировки.

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

5.1. Общие блокировки с использованием FileInputStream

FileChannel , полученный из FileInputStream , доступен для чтения. Таким образом, мы можем получить общую блокировку:

try (FileInputStream fileInputStream = new FileInputStream("/tmp/testfile.txt");
FileChannel channel = fileInputStream.getChannel();
FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
// read from the channel
}

В приведенном выше фрагменте вызов lock() на канале завершится успешно. Это потому, что общая блокировка требует только, чтобы канал был доступен для чтения. Это тот случай, когда мы создали его из FileInputStream .

5.2. Общие блокировки с использованием RandomAccessFile

На этот раз мы можем открыть файл только с правами на чтение :

try (RandomAccessFile file = new RandomAccessFile("/tmp/testfile.txt", "r"); 
FileChannel channel = file.getChannel();
FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
// read from the channel
}

В этом примере мы создали RandomAccessFile с разрешениями на чтение. Мы можем создать из него читаемый канал и, таким образом, создать общий замок.

5.3. Для общих блокировок требуется доступный для чтения FileChannel

По этой причине мы не можем получить общую блокировку через FileChannel , созданный из FileOutputStream :

Path path = Files.createTempFile("foo","txt");
try (FileOutputStream fis = new FileOutputStream(path.toFile());
FileLock lock = fis.getChannel().lock(0, Long.MAX_VALUE, true)) {
// unreachable code
} catch (NonWritableChannelException e) {
// handle exception
}

В этом примере вызов lock() пытается получить общую блокировку канала, созданного из FileOutputStream . Такой канал доступен только для записи. Это не удовлетворяет потребности в том, чтобы канал был читабельным. Это вызовет исключение NonWritableChannelException .

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

6. Что следует учитывать

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

В системах POSIX блокировки являются рекомендательными. Различные процессы, читающие или записывающие в данный файл, должны согласовать протокол блокировки. Это обеспечит целостность файла. Сама ОС не будет применять блокировку.

В Windows блокировки будут эксклюзивными, если не разрешено совместное использование. Обсуждение преимуществ и недостатков механизмов, специфичных для ОС, выходит за рамки этой статьи. Тем не менее, важно знать эти нюансы при реализации механизма блокировки.

7. Заключение

В этом руководстве мы рассмотрели несколько различных вариантов получения блокировки файлов в Java.

Во-первых, мы начали с понимания двух основных механизмов блокировки и того, как библиотека Java NIO облегчает блокировку файлов. Затем мы рассмотрели ряд простых примеров, показывающих, что мы можем получать эксклюзивные и общие блокировки в наших приложениях. Мы также рассмотрели типы типичных исключений, с которыми мы можем столкнуться при работе с блокировками файлов.

Как всегда, исходный код примеров доступен на GitHub .