Specifically, the behaviour I am looking for is this:
Read operations happen concurrently, and will execute once all pending write operations are finished.
Write operations always wait until all other read/write operations are completed.
Close operations always wait until all other read/write operations are completed.
In other words, these operations should be enqueued.
The official documentation for NIO FileLocks does not specify this behaviour. Infact it states that:
File locks are held on behalf of the entire Java virtual machine. They
are not suitable for controlling access to a file by multiple threads
within the same virtual machine.
I have played with idea of manually enqueing all the requests and calling get() on all outstanding Futures before submitting a new I/O request
but I have no idea if this is even a good idea.
How can I achieve this behaviour?
EDIT: Thanks to fge’s insights I have managed to find a basic solution to my problem:
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ChannelAccessFactory {
public static final ExecutorService IO_THREADS = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private final Path file;
private final ReadWriteLock lock;
public ChannelAccessFactory (Path file){
this.file = file;
this.lock = new ReentrantReadWriteLock();
}
public ReadWriteLock getLock(){
return lock;
}
public ChannelAccess newAccess() throws Exception{
return new ChannelAccess(file, lock);
}
}
Wrapped Channel class:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.locks.ReadWriteLock;
public class ChannelAccess implements AutoCloseable{
private final ReadWriteLock lock;
private final AsynchronousFileChannel channel;
protected ChannelAccess (Path file, ReadWriteLock lock) throws Exception{
this.lock = lock;
this.channel = AsynchronousFileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE);
}
public Future<Integer> read(final ByteBuffer buffer, final long position){
return ChannelAccessFactory.IO_THREADS.submit(new Callable<Integer>(){
@Override
public Integer call() throws InterruptedException, ExecutionException{
lock.readLock().lock();
try{
return channel.read(buffer, position).get();
}
finally {
lock.readLock().unlock();
}
}
});
}
public Future<Integer> write(final ByteBuffer buffer, final long position){
return ChannelAccessFactory.IO_THREADS.submit(new Callable<Integer>(){
@Override
public Integer call() throws InterruptedException, ExecutionException {
lock.writeLock().lock();
try{
return channel.write(buffer, position).get();
}
finally {
lock.writeLock().unlock();
}
}
});
}
public long size() throws Exception{
lock.readLock().lock();
try{
return channel.size();
}
finally{
lock.readLock().unlock();
}
}
@Override
public void close() {
lock.readLock().lock();
try{
channel.close();
}
catch (IOException e){}
finally{
lock.readLock().unlock();
}
}
}
Use a
ReentrantReadWriteLock. It allows many concurrent readers but only one writer.It is not a “pure” NIO solution but it is a primitive which behaves the way you want.
Beware: use locks like this to avoid deadlocks: