Supongamos el siguiente escenario,
Tenemos una proceso que puede ser ejecutado desde multiples lugares, incluso de forma simultánea, y un recurso que es compartido por dichas ejecuciones (puede ser un archivo, una carpeta, un URL, un proceso, etc) .
Necesitamos asegurarnos, que mientras se está utilizando dicho recurso este no sea modificado, o que mientras lo estemos modificando, este no sea utilizado por otros procesos.
Tomemos como ejemplo, que tenemos un proceso que lee el contenido (archivos, carpetas) de una ruta y ejecuta alguna lógica. Dicho proceso puede ser ejecutado de forma simultánea 0 o más veces. Sin embargo, el contenido de dicha ruta puede cambiar cada cierto tiempo: eliminar archivos, agregar carpetas, o incluso modificar el contenido de uno o más archivos.
Deseamos implementar entonces un mecanismo de bloqueo que me permita un uso exclusivo del archivo para escritura, y a la vez un uso compartido durante la lectura. Esto lo haremos usando la librería Java NIO.
Manos a la obra
Crearemos un archivo, este lo pueden ubicar donde gusten, en este caso lo ubicaremos en la raíz del directorio que tenemos como recurso. Y llamaremos a dicho archivo como .lock
.No debemos preocuparnos por el contenido de dicho archivo, solamente utilizaremos las opciones de Java NIO sobre este archivo para habilitar el mecanismo de bloqueo.
Bloqueo durante Lectura
Para realizar una acción de lectura vamos a utilizar el siguiente código.
Path lockFilePath = Paths.get("/my/path/.lock"); try (var channel = FileChannel.open(lockFilePath, StandardOpenOption.READ, StandardOpenOption.CREATE); FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) { //Your logic here }
- Utilizamos el método
lock
delFileChannel
de nuestro archivo.lock
. - La opción
StandardOpenOption.READ
indica que estamos accediendo en modo lectura, lo que permite que otros proceso de lectura también puedan usar el archivo de forma simultánea. - La opción
StandardOpenOption.CREATE
nos crea el archivo .lock en caso que no existiera (primer uso)
Bloqueo durante Escritura
Para aplicar una acción de escritura vamos a utilizar el siguiente código.
Path lockFilePath = Paths.get("/my/path/.lock"); try (var channel = FileChannel.open(lockFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE); FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) { //Your logic here }
- Utilizamos el método
lock
delFileChannel
de nuestro archivo.lock
. - La opción
StandardOpenOption.WRITE
indica que estamos accediendo en modo escritura, lo que no permite que otros procesos de lectura o escritura estén accediendo al mismo tiempo. - La opción
StandardOpenOption.CREATE
nos crea el archivo .lock en caso que no existiera (primer uso)
Si por alguna razón tenemos una lógica que está en modo escritura, y otro proceso de escritura o lectura intenta obtener un bloqueo sobre el archivo .lock
, eso no va a ser posible. Dicho proceso se va a quedar esperando hasta que el proceso inicial de escritura finalice, y solo hasta ese momento, va a continuar.
De igual manera, si hay uno o varios procesos en modo lectura, y un proceso de escritura trata de bloquear el archivo .lock
, esta escritura va a quedar esperando hasta que los proceso de lectura terminen.
Simplificando nuestro código
Ahora que entendemos las bases, podemos crear una clase que nos ayude a simplificar dicho código, y hacerlo más genérico de manera que podemos usarlo con múltiples propósitos.
public class { private final Path lockFilePath; public FileLockHelper(String path){ lockFilePath = Paths.get(path); } public <T> T readLock(Executable<T> function) throws IOException { try (var channel = FileChannel.open(lockFilePath, StandardOpenOption.READ, StandardOpenOption.CREATE); FileLock lock = channel.lock()) { return function.execute(); } } public <T> T writeLock(Executable<T> function) throws IOException { try (var channel = FileChannel.open(lockFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE); FileLock lock = channel.lock()) { return function.execute(); } } @FunctionalInterface public interface Executable<V> { V execute() throws IOException; } }
Creamos una interface funcional Executable
para ser recibida en nuestros métodos, y ejecutarla como una expresión lambda. De esta manera podemos incluir cualquier lógica dentro de los métodos writeLock
y readLock
según nos convenga.
De esta manera, podemos utilizar nuestro FileLockHelper
FileLockHelper fileLockHelper = new FileLockHelper("/my/path/.lock"); fileLockHelper.writeLock(() -> { //Add my logic here return 1; }) fileLockHelper.read(() -> { //Add my logic here return "String"; })
Conclusiones
Este mecanismo se puede utilizar para múltiples propósitos, no solamente el ejercicio que fue presentado en este artículo. Asegurar la integridad de nuestros datos es muy importante, sobre todo cuando tenemos aplicaciones que acceden de forma concurrente a un mismo recurso.
Aplicar mecanismos de bloqueo no es tarea fácil, pero con un poco de imaginación se pueden encontrar alternativas sencillas que pueden ayudarnos para muchos escenarios, como esta que hemos presentando. Esperando que sea de ayuda para muchas personas.