1. Información general
Al leer o escribir archivos, debemos asegurarnos de que los mecanismos adecuados de bloqueo de archivos estén en su lugar. Esto asegura la integridad de los datos en aplicaciones concurrentes basadas en E / S.
En este tutorial, veremos varios enfoques para lograr esto utilizando la biblioteca Java NIO .
2. Introducción a los bloqueos de archivos
En general, existen dos tipos de cerraduras :
-
- Bloqueos exclusivos, también conocidos como bloqueos de escritura
- Bloqueos compartidos: también denominados bloqueos de lectura
En pocas palabras, un bloqueo exclusivo evita todas las demás operaciones, incluidas las lecturas, mientras se completa una operación de escritura.
Por el contrario, un candado compartido permite que más de un proceso lea al mismo tiempo. El objetivo de un bloqueo de lectura es evitar la adquisición de un bloqueo de escritura por otro proceso. Normalmente, cualquier proceso debería poder leer un archivo en un estado coherente.
En la siguiente sección, veremos cómo Java maneja estos tipos de bloqueos.
3. Bloqueos de archivos en Java
La biblioteca Java NIO permite bloquear archivos a nivel de sistema operativo. Los métodos lock () y tryLock () de un FileChannel son para ese propósito.
Podemos crear un FileChannel a través de FileInputStream , FileOutputStream o RandomAccessFile . Los tres tienen un método getChannel () que devuelve un FileChannel .
Alternativamente, podemos crear un FileChannel directamente a través del método abierto estático :
try (FileChannel channel = FileChannel.open(path, openOptions)) { // write to the channel }
A continuación, revisaremos diferentes opciones para obtener bloqueos exclusivos y compartidos en Java. Para obtener más información sobre los canales de archivos, consulte nuestro tutorial Guía de Java FileChannel.
4. Cerraduras exclusivas
Como ya hemos aprendido, al escribir en un archivo, podemos evitar que otros procesos lean o escriban en él utilizando un bloqueo exclusivo .
Obtenemos bloqueos exclusivos llamando a lock () o tryLock () en la clase FileChannel . También podemos usar sus métodos sobrecargados:
- bloqueo (posición larga, tamaño largo, booleano compartido)
- tryLock (posición larga, tamaño largo, booleano compartido)
En esos casos, el parámetro compartido debe establecerse en falso .
Para obtener un bloqueo exclusivo, debemos usar un FileChannel escribible . Podemos crearlo a través de los métodos getChannel () de FileOutputStream o RandomAccessFile . Alternativamente, como se mencionó anteriormente, podemos usar el método abierto estático de la clase FileChannel . Todo lo que necesitamos es establecer el segundo argumento en StandardOpenOption.APPEND :
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.APPEND)) { // write to channel }
4.1. Bloqueos exclusivos mediante FileOutputStream
Un FileChannel creado a partir de FileOutputStream se puede escribir. Podemos, por tanto, adquirir una cerradura exclusiva:
try (FileOutputStream fileOutputStream = new FileOutputStream("/tmp/testfile.txt"); FileChannel channel = fileOutputStream.getChannel(); FileLock lock = channel.lock()) { // write to the channel }
Aquí, channel.lock () bloqueará hasta que obtenga un bloqueo, o lanzará una excepción. Por ejemplo, si la región especificada ya está bloqueada, se lanza una OverlappingFileLockException . Consulte el Javadoc para obtener una lista completa de posibles excepciones.
También podemos realizar un bloqueo sin bloqueo usando channel.tryLock () . Si no consigue un bloqueo porque otro programa tiene uno superpuesto, devuelve nulo . Si no lo hace por cualquier otro motivo, se lanza una excepción apropiada.
4.2. Bloqueos exclusivos mediante un archivo RandomAccessFile
Con un RandomAccessFile , necesitamos establecer banderas en el segundo parámetro del constructor.
Aquí, vamos a abrir el archivo con permisos de lectura y escritura:
try (RandomAccessFile file = new RandomAccessFile("/tmp/testfile.txt", "rw"); FileChannel channel = file.getChannel(); FileLock lock = channel.lock()) { // write to the channel }
Si abrimos el archivo en modo de solo lectura e intentamos escribir en su canal, arrojará una NonWritableChannelException .
4.3. Los bloqueos exclusivos requieren un canal de archivos grabable
Como se mencionó anteriormente, los bloqueos exclusivos necesitan un canal de escritura. Por lo tanto, no podemos obtener un bloqueo exclusivo a través de un FileChannel creado a partir de un 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 }
En el ejemplo anterior, el método lock () arrojará una NonWritableChannelException . De hecho, esto se debe a que estamos invocando getChannel en un FileInputStream , que crea un canal de solo lectura.
Este ejemplo es solo para demostrar que no podemos escribir en un canal no modificable. En un escenario del mundo real, no detectaríamos y volveríamos a lanzar la excepción.
5. Cerraduras compartidas
Recuerde, los bloqueos compartidos también se denominan bloqueos de lectura . Por lo tanto, para obtener un bloqueo de lectura, debemos usar un FileChannel legible .
Dicho FileChannel se puede obtener llamando al método getChannel () en un FileInputStream o un RandomAccessFile . Nuevamente, otra opción es usar el método abierto estático de la clase FileChannel . En ese caso, establecemos el segundo argumento en StandardOpenOption.READ :
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ); FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) { // read from the channel }
Una cosa a tener en cuenta aquí es que elegimos bloquear todo el archivo llamando a lock (0, Long.MAX_VALUE, true) . También podríamos haber bloqueado solo una región específica del archivo cambiando los dos primeros parámetros a valores diferentes. El tercer parámetro debe establecerse en verdadero en el caso de un bloqueo compartido.
Para simplificar las cosas, bloquearemos todo el archivo en todos los ejemplos siguientes, pero tenga en cuenta que siempre podemos bloquear una región específica de un archivo.
5.1. Bloqueos compartidos mediante FileInputStream
Un FileChannel obtenido de un FileInputStream es legible. Por lo tanto, podemos obtener un bloqueo compartido:
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 }
En el fragmento anterior, la llamada a lock () en el canal se realizará correctamente. Eso es porque un candado compartido solo requiere que el canal sea legible. Es el caso aquí, ya que lo creamos a partir de un FileInputStream .
5.2. Bloqueos compartidos mediante un archivo RandomAccess
Esta vez, podemos abrir el archivo con solo leer permisos:
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 }
En este ejemplo, creamos un RandomAccessFile con permisos de lectura. Podemos crear un canal legible a partir de él y, así, crear un candado compartido.
5.3. Los bloqueos compartidos requieren un canal de archivos legible
Por esa razón, no podemos adquirir un bloqueo compartido a través de un FileChannel creado a partir de un 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 }
In this example, the call to lock() tries to get a shared lock on a channel created from a FileOutputStream. Such a channel is write-only. It doesn't fulfil the need that the channel has to be readable. This will trigger a NonWritableChannelException.
Again, this snippet is just to demonstrate that we can't read from a non-readable channel.
6. Things to Consider
In practice, using file locks is difficult; the locking mechanisms aren’t portable. We’ll need to craft our locking logic with this in mind.
In POSIX systems, locks are advisory. Different processes reading or writing to a given file must agree on a locking protocol. This will ensure the file’s integrity. The OS itself won’t enforce any locking.
En Windows, los bloqueos serán exclusivos a menos que se permita compartir. Discutir los beneficios o inconvenientes de los mecanismos específicos del sistema operativo está fuera del alcance de este artículo. Sin embargo, es importante conocer estos matices al implementar un mecanismo de bloqueo.
7. Conclusión
En este tutorial, hemos revisado varias opciones diferentes para obtener bloqueos de archivos en Java.
Primero, comenzamos por comprender los dos mecanismos de bloqueo principales y cómo la biblioteca Java NIO facilita el bloqueo de archivos. Luego recorrimos una serie de ejemplos simples que muestran que podemos obtener bloqueos exclusivos y compartidos en nuestras aplicaciones. También echamos un vistazo a los tipos de excepciones típicas que podemos encontrar al trabajar con bloqueos de archivos.
Como siempre, el código fuente de los ejemplos está disponible en GitHub.