Блокировка файлов для совместного использования

Содержание

1 Введение
2 Демонстрация функций блокировки
3 Использование массивов нулевой длины
4 Нулевой массив и использование ArrayList
5 Ссылки и дополнительная информация

Введение

Возникала ли у вас когда-либо потребность разделить доступ к внешнему файлу с не Java-приложениями или среди нескольких Java-приложений? До выпуска версии 1.4 платформы J2SE этого нельзя было сделать без использования внутреннего кода. Сейчас, благодаря функции FileChannel/FileLock New I/O (NIO) API в J2SE v 1.4, у вас есть безопасный способ разделения доступа к внешнему файлу без необходимости изменения внутреннего кода.

Класс FileLock, являющийся частью пакета java.nio.channels, предоставляет блокировку файла. Вы можете использовать блокировку файла для ограничения доступа к нему из нескольких процессов. Кроме того, вы имеете возможность ограничить доступ ко всему файлу, или только к небольшой его части. Блокировка файла может быть двух типов - с обеспечением совместного доступа или с монополизацией. Блокировка с обеспечением совместного доступа поддерживает доступ по чтению из нескольких процессов, в то время как блокировка с монополизацией обычно используется для записи. Поскольку блокировка осуществляется на уровне файла, вы не должны использовать ее для ограничения доступа к файлу различных потоков одного процесса. Если вы примените блокировку файла в нескольких потоках, ни один из потоков не будет ограничен в доступе к файлу. Ограничения будут иметь только внешние процессы.

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

  • FileInputStream

  • FileOutputStream

  • RandomAccessFile

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

FileLock lock()
FileLock tryLock()

Не имеющие аргументов версии этих методов делают попытку получить блокировку с монополизацией доступа ко всему файлу, связанному с каналом. Метод lock не возвращает управление до тех пор, пока блокировка не подтвердится. Метод tryLock возвращает null, если другой процесс в это время заблокировал часть файла.

FileLock lock(long position, long size, boolean shared)
FileLock tryLock(long position, long size, boolean shared)

Версии с тремя аргументами дают возможность заблокировать часть файла. Аргумент position указывает начальную позицию блокировки. Аргумент size указывает количество байт для блокировки. Значение shared, равное true, означает, что производится блокировка с разделением доступа, а значение false означает, что производится блокировка с монополизацией. Аргумент size не ограничен размерами файла. Это дает возможность заблокировать часть файла до его записи. Например, версии lock и tryLock с тремя аргументами используют диапазон от 0 до Long.MAX_VALUE.

Все методы блокировки файлов класса FileChannel возвращают FileLock. Вам он необходим для снятия блокировки. Как и для поточной блокировки, лучшим подходом является удержание блокировки файла наименьшее количество времени. FileLock предлагает следующие методы:

  • channel - возвращает источник FileChannel для блокировки

  • isShared - указывает, является ли блокировка блокировкой с разделением доступа

  • isValid - указывает, актуальна ли еще блокировка (блокировка не является актуальной, если канал файла закрыт)

  • overlaps(long position, long size) - проверяет наложение с указанной областью

  • position - возвращает начальную позицию заблокированной области

  • release - снимает блокировку

  • size - возвращает размер заблокированной области в байтах

  • toString - возвращает строковое представление блокировки

Демонстрация функций блокировки

Ниже приведена программа, демонстрирующая функции блокировки файлов. Программа применяет к файлу блокировку с монополизацией, сигнализирует о получении блокировки и, далее, ждет, пока вы не нажмете клавишу Enter. Попробуйте выполнить программу одновременно в двух окнах. Вы увидите, что во втором окне программа работать не будет до тех пор, пока вы не нажмете Enter в первом окне. (Обратите внимание, что если в текущем каталоге есть файл с именем junk.dat, он будет перезаписан.)

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

public class Locking {
 
public static void main(String arsg[]) throws IOException {
   
RandomAccessFile raf = new RandomAccessFile("junk.dat", "rw");
    FileChannel channel = raf.getChannel
();
    FileLock lock = channel.lock
();
   
try {
     
System.out.println("Got lock!!!");
      System.out.println
("Press ENTER to continue");
      System.in.read
(new byte[10]);
   
} finally {
     
lock.release();
   
}
  }
}

Важно понимать, что не имеет значения, заблокирован ли файл из программы, написанной на языке C/C++, или при помощи FileChannel другой Java-программы. Процедуры ожидания и получения блокировки не зависят от источника блокировки файла.

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

Использование массивов нулевой длины

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

Как можно реализовать метод фильтрации такого типа? Вот один из вариантов:

import java.util.*;

public class ZeroDemo1 {

 
// отфильтровать входной массив и отбросить
  // значения, которые меньше чем minval или больше чем
  // maxval

 
static int[] filterData(int indata[], int minval, int maxval) {

   
// проверить параметры на ошибки

   
if (indata == null) {
     
throw new NullPointerException("indata is null");
   
}
   
if (maxval < minval) {
     
throw new IllegalArgumentException("maxval < minval");
   
}

   
// подсчитать количество правильных значений
    // во входном массиве

   
int validcnt = 0;
   
for (int i = 0; i < indata.length; i++) {
     
if (indata[i] >= minval && indata[i] <= maxval) {
       
validcnt++;
     
}
    }

   
// если нет правильных значений, возвратить null

   
if (validcnt == 0) {
     
return null;
   
}

   
// скопировать правильные значения в новый массив
    // и возвратить его

   
int outdata[] = new int[validcnt];
   
for (int i = 0, j = 0; i < indata.length; i++) {
     
if (indata[i] >= minval && indata[i] <= maxval) {
       
outdata[j++] = indata[i];
     
}
    }
   
return outdata;
 
}

 
public static void main(String args[]) {

   
// создать тестовый массив целых

   
int indata[] = new int[] { 1, 3, -17, 8, 59 };

   
// отфильтровать значения, не входящие в диапазон 1-10

   
int outdata1[] = filterData(indata, 1, 10);
   
for (int i = 0; i < outdata1.length; i++) {
     
System.out.println(outdata1[i]);
   
}

   
// отфильтровать значения,
    // не входящие в диапазон 100-200

   
int outdata2[] = filterData(indata, 100, 200);
   
for (int i = 0; i < outdata2.length; i++) {
     
System.out.println(outdata2[i]);
   
}
  }
}

Метод filterData выполняет два просмотра входного массива. За первый просмотр подсчитывается количество данных с корректными значениями. Затем метод размещает новый массив соответствующего размера и копирует в него подходящие данные. Если таких данных нет, метод возвращает значение null в качестве ссылки на массив.

Вот результат выполнения программы ZeroDemo1:

1
3
8
Exception in thread "main"
java.lang.NullPointerException
at ZeroDemo1.main
(ZeroDemo1.java:72)

Проблема, возникающая в этой программе, довольно типична. Второй вызов filterData возвращает значение null и происходит сбой программы.

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

/*
if (validcnt == 0) {
    return null;
}
*/

Если корректных данных нет, управление переходит к следующей строке, которая размещает массив с нулевой длиной:

int outdata[] = new int[0];

Это совершенно легальное выражение языка Java. Представление Java-массивов включает в себя длину массива и, следовательно, возможно указать, что длина массива равна нулю.

Если вы ожидаете, что в примере ZeroDemo1 validcnt часто будет равен нулю, то есть, после фильтрации данных из входных данных часто будут удаляться все значения, вы можете оптимизировать второе сканирование данных, добавив, например, следующий код:

int outdata[] = new int[validcnt];
if (validcnt == 0) {
   
return outdata;
}

Отметим, что следующая строка:

int outdata[] = new int[]{};

тоже корректна для инициализации массива набором целых констант, имеющим нулевую длину.

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

private static final int ZERO_LENGTH_ARRAY[] = new int[0];

Этот массив является постоянным (его нельзя изменить) и может использоваться совместно во всем приложении.

Нулевой массив и использование ArrayList

Существует еще один способ использования массивов с нулевой длиной. Этот способ показан в следующем примере:

import java.util.*;

public class ZeroDemo2 {
 
public static void main(String args[]) {

   
// создать ArrayList и добавить в него строки

   
List stringlist = new ArrayList();
    stringlist.add
("string 1");
    stringlist.add
("string 2");
    stringlist.add
("string 3");

   
// преобразовать в массив элементов с типом String

   
String out[] = (String[]) stringlist.toArray(new String[0]);
   
for (int i = 0; i < out.length; i++) {
     
System.out.println(out[i]);
   
}
  }
}

Вот результат выполнения программы ZeroDemo2:

string 1
string 2
string 3

Программа ZeroDemo2 создает ArrayList и добавляет в него три строки. Затем вызывается toArray для преобразования массива из трех элементов, имеющих тип String, в ArrayList. В этом примере аргументом в toArray является "new String[0]". Такой аргумент выполняет две задачи. Во-первых, если имеется массив элементов типа String, являющийся достаточно большим для хранения элементов в ArrayList, можно указать его в качестве аргумента toArray. Метод будет использовать массив String.

Но если массив не достаточно большой, то метод toArray размещает массив для возврата элементов. Он использует тип переданного ему массива для определения типа размещаемого массива. ArrayList записывает ссылки на элементы в массив типа Object. Методу toArray необходимо указать, существует ли какой-либо другой тип (такой как String), используемый для возврата массива элементов. Метод применяет отображение (java.lang.reflect.Array.newInstance) для создания массива соответствующего типа.

Ссылки и дополнительная информация

Дополнительная информация по блокировкам файлов, а так же другие функции NIO API, находятся в руководстве "New I/O API".

Дополнительная информация об использовании массивов нулевой длины находится в разделе 27 "Возврат массивов нулевой длины, а не null" в книге "Эффективное руководство по языку программирования Java" Joshua Bloch.

Теги: file chanels files