How to Implement File Locking Feature in Java
As we know within the java application we can avoid concurrent writes/read from different threads using synchronization/Locks as shown below.
-
Using synchronized keyword we can make complete method thread safe.
-
Using Lock we can make some portion of the code thread safe.
Synchronization.java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Synchronization{
public synchronized void write(){
//write some logic to avoid concurrent writes by different threads.
}
public void read(){
//some portion of the code
Lock lock=new ReentrantLock(true);
if(lock.tryLock()){
//some portion of code
lock.unlock();
}
//some portion of the code
}
}
What if we want to avoid concurrent writes on a file by different applications/processes?
java.nio.channels.FileChannel will help us to achieve this.
Application.java
package com.codergists.nio.files;
import com.codergists.nio.files.locks.ISessionFileLock;
import com.codergists.nio.files.model.Session;
import lombok.extern.slf4j.Slf4j;
import static com.codergists.nio.files.util.FileLockUtil.getSessionFileAbsolutePath;
import static java.nio.file.Files.deleteIfExists;
import static java.nio.file.Paths.get;
@Slf4j
public class Application {
public static void main(String[] args) throws Exception {
//create
try (ISessionFileLock sessionFileLock = ISessionFileLock.Builder.createNewFileLock()) {
Session session = new Session();
session.setId("1");
session.setName("FileLockingTest");
sessionFileLock.write(session);
}
//read, write
try (ISessionFileLock sessionFileLock = ISessionFileLock.Builder.openReadWriteLock()) {
Session session = sessionFileLock.read();
session.setId("2");
sessionFileLock.write(session);
}
//readOnly
try (ISessionFileLock sessionFileLock = ISessionFileLock.Builder.openReadOnlyLock(get(getSessionFileAbsolutePath()))) {
Session session = sessionFileLock.read();
log.info("session {}", session);
}
deleteIfExists(get(getSessionFileAbsolutePath()));
}
}
Below interface contains ISessionFileLock supported methods.
ISessionFileLock.java
package com.codergists.nio.files.locks;
import com.codergists.nio.files.exception.SessionFileLockException;
import com.codergists.nio.files.model.Session;
import java.nio.channels.FileLock;
import java.nio.file.Path;
/**
* Interface to {@link FileLock} implementation
*
* @author codergists
*/
public interface ISessionFileLock extends AutoCloseable {
/**
* Deserialization(json file to java object) of session file
*
* @return session {@link Session}
*
* @throws SessionFileLockException when failed
*/
Session read() throws SessionFileLockException;
/**
* Serialization(java object to json file) of session file
*
* @param session {@link Session}
*
* @throws SessionFileLockException when failed
*/
void write(Session session) throws SessionFileLockException;
/**
* close and release session lock
*/
@Override
void close();
/**
* fileLock builder to open differentTypes Lock
*/
class Builder {
/**
* To create new session file
*
* @return FileLock {@link FileLock}
*
* @throws SessionFileLockException failed to open
*/
public static ISessionFileLock createNewFileLock() throws SessionFileLockException {
SessionFileLock fileLock = new SessionFileLock();
fileLock.createFileChannel();
fileLock.acquireLock();
return fileLock;
}
/**
* To read and write existing session file
*
* @return FileLock {@link FileLock}
*
* @throws SessionFileLockException when session file doesn't exists
*/
public static ISessionFileLock openReadWriteLock() throws SessionFileLockException {
SessionFileLock fileLock = new SessionFileLock();
fileLock.readWriteFileChannel();
fileLock.acquireLock();
return fileLock;
}
/**
* To open for readOnly, you may expect dirty reads
*
* @param filePath session file location
*
* @return FileLock {@link FileLock}
*
* @throws SessionFileLockException when file doesn't exists
*/
public static ISessionFileLock openReadOnlyLock(Path filePath) throws SessionFileLockException {
SessionFileLock fileLock = new SessionFileLock();
fileLock.readOnlyFileChannel(filePath);
//fileLock.acquireLock(); don't acquireLock on readOnlyChannel
return fileLock;
}
}
}
Session.java
package com.codergists.nio.files.model;
import lombok.ToString;
import java.io.Serializable;
@ToString
public class Session implements Serializable {
private String id;
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
SessionFileLockException.java
package com.codergists.nio.files.exception;
public class SessionFileLockException extends RuntimeException {
public SessionFileLockException() {
}
public SessionFileLockException(String message) {
super(message);
}
public SessionFileLockException(String message, Throwable cause) {
super(message, cause);
}
public SessionFileLockException(Throwable cause) {
super(cause);
}
public SessionFileLockException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
Implementation about ISessionFileLock interface shown below.
SessionFileLock.java
package com.codergists.nio.files.locks;
import com.codergists.nio.files.exception.SessionFileLockException;
import com.codergists.nio.files.model.Session;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.TimeUnit;
import static com.codergists.nio.files.util.FileLockUtil.getSessionFileAbsolutePath;
import static java.nio.file.Paths.get;
@Slf4j
public final class SessionFileLock implements ISessionFileLock {
/**
* Instance of OBJECT_MAPPER
*/
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* exception message
*/
private static final String SESSION_FILE_NOT_EXISTS = "Session file not exists";
static {
OBJECT_MAPPER.registerModule(new JavaTimeModule());
OBJECT_MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
OBJECT_MAPPER.configure(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS, true);
OBJECT_MAPPER.configure(SerializationFeature.INDENT_OUTPUT, true);
OBJECT_MAPPER.setPropertyNamingStrategy(new PropertyNamingStrategies.KebabCaseStrategy());
OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
/**
* fileChannel
*/
private FileChannel fileChannel;
/**
* fileLock
*/
private FileLock fileLock;
/**
* private constructor
*/
SessionFileLock() {
}
/**
* This channel allows us to create new session file. if file already exists it throws exception
* use this only to create new session file.
*
* @throws SessionFileLockException when session file already exists
*/
void createFileChannel() throws SessionFileLockException {
try {
this.fileChannel = FileChannel.open(get(getSessionFileAbsolutePath()),
StandardOpenOption.WRITE,
StandardOpenOption.CREATE_NEW);
} catch (FileAlreadyExistsException alreadyExistsException) {
log.error("Session already exists", alreadyExistsException);
throw new SessionFileLockException("active session already exists", alreadyExistsException);
} catch (IOException ioException) {
log.error("Failed to create new file", ioException);
throw new SessionFileLockException(ioException.getMessage(), ioException);
}
}
/**
* This channel only used to read sessionFile, {@link #acquireLock()} not allowed on this channel.
* as this is not allowed to acquire locks ,it allows dirty reads.
*
* @param filePath filePath
*
* @throws SessionFileLockException when session doesn't exists
*/
void readOnlyFileChannel(Path filePath) throws SessionFileLockException {
try {
this.fileChannel = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.READ);
} catch (NoSuchFileException noSuchFileException) {
log.debug(SESSION_FILE_NOT_EXISTS, noSuchFileException);
throw new SessionFileLockException(SESSION_FILE_NOT_EXISTS, noSuchFileException);
} catch (IOException ioException) {
log.error("Failed to read file", ioException);
throw new SessionFileLockException(ioException.getMessage(), ioException);
}
}
/**
* This channel used to read and update session after acquiring lock. this channel expects file already exists
* if file not found it throws {@link NoSuchFileException}.
*
* @throws SessionFileLockException when session doesn't exists
*/
void readWriteFileChannel() throws SessionFileLockException {
try {
this.fileChannel = FileChannel.open(
get(getSessionFileAbsolutePath()),
StandardOpenOption.READ,
StandardOpenOption.WRITE);
} catch (NoSuchFileException noSuchFileException) {
log.debug(SESSION_FILE_NOT_EXISTS, noSuchFileException);
throw new SessionFileLockException(SESSION_FILE_NOT_EXISTS, noSuchFileException);
} catch (IOException ioException) {
log.error("Failed to read file", ioException);
throw new SessionFileLockException(ioException.getMessage(), ioException);
}
}
/**
* Try to acquire lock of the session file
*
* @throws SessionFileLockException failed to acquire lock
*/
void acquireLock() throws SessionFileLockException {
try {
log.debug("Trying to acquire lock on fileChannel");
FileLock _lock = fileChannel.tryLock();
while (null == _lock) {//it only enter into loop if lock not acquired
log.warn("Unable to acquire lock on fileChannel, This attempt will be retried after 1 second");
waitForSeconds(1);
_lock = fileChannel.tryLock();
}
log.debug("Acquired lock. isValid? {}, isShared {}", _lock.isValid(), _lock.isShared());
this.fileLock = _lock;
} catch (IOException ioException) {
log.error("Failed to acquire lock", ioException);
throw new SessionFileLockException("Failed to acquire lock", ioException);
}
}
private void waitForSeconds(int i) {
try {
TimeUnit.SECONDS.sleep(i);
} catch (Exception e) {
log.debug("Interrupted ", e);
}
}
/**
* Read and convert into java object (de-serialization)
*
* @return Student {@link Session}
*
* @throws SessionFileLockException if unable to read Student
*/
@Override
public Session read() throws SessionFileLockException {
try {
ByteBuffer buf = ByteBuffer.allocate(5 * 1024 * 1024);//5MB
fileChannel.read(buf);
final Session Student = OBJECT_MAPPER.readValue(buf.array(), Session.class);
return Student;
} catch (IOException ioException) {
log.warn("Unable to read file", ioException);
throw new SessionFileLockException(ioException.getMessage(), ioException);
}
}
/**
* Write java object into into disk (serialization)
*
* @param Student {@link Session}
*
* @throws SessionFileLockException failed to write to disk
*/
@Override
public void write(Session Student) throws SessionFileLockException {
try {
//must truncate size before writing. to overwrite the file content
fileChannel.truncate(0);
int c = fileChannel.write(getBytes(Student), 0);
log.info("Successfully saved to disk, Total bytes {}", c);
} catch (NoSuchFileException noSuchFileException) {//for readWriteFileChannel
log.warn(SESSION_FILE_NOT_EXISTS, noSuchFileException);
throw new SessionFileLockException(SESSION_FILE_NOT_EXISTS, noSuchFileException);
} catch (FileAlreadyExistsException alreadyExistsException) {//for createFileChannel
log.error("Session already exists exception", alreadyExistsException);
throw new SessionFileLockException("active session already exists", alreadyExistsException);
} catch (IOException ioException) {
log.error("Error while writing into file", ioException);
throw new SessionFileLockException(ioException.getMessage(), ioException);
}
}
/**
* Converts java object into bytes
*
* @param Student {@link Session}
*
* @return bytes
*
* @throws SessionFileLockException when failed
*/
private ByteBuffer getBytes(final Session Student) throws SessionFileLockException {
try {
log.debug("Converting Student object to bytes");
return ByteBuffer.wrap(OBJECT_MAPPER.writeValueAsBytes(Student));
} catch (Exception exception) {
log.warn("Failed to parse", exception);
throw new SessionFileLockException(exception);
}
}
/**
* Close lock and channels
*/
@Override
public void close() {
releaseLock();
}
/**
* Release lock and close fileChannel
*/
private void releaseLock() {
try {
if (null != fileLock && fileLock.isValid()) {
fileLock.release();
log.debug("FileChannel Lock {} released successfully", fileLock);
}
} catch (IOException exception) {
log.warn("Failed to release lock", exception);
}
releaseChannel();
}
/**
* release fileChannel
*/
private void releaseChannel() {
try {
if (null != fileChannel && fileChannel.isOpen()) {
fileChannel.close();
log.debug("Channel {} closed successfully", fileChannel);
}
} catch (Exception exception) {
log.warn("Failed to close channel", exception);
}
}
}
Repo
Full code available at https://github.com/tvajjala/nio-file-locking.git
Comments
Post a Comment