I would implement this as follows:
interface FileOperator {
public void operate(File file);
}
class FileProxy {
private static final ConcurrentHashMap<URI, FileProxy> map =
new ConcurrentHashMap<>();
private final Semaphore mutex = new Semaphore(1, true);
private final File file;
private final URI key;
private FileProxy(File file) {
this.file = file;
this.key = file.toURI();
}
public static void operate(URI uri, FileOperator operator) {
FileProxy curProxy = map.get(uri);
if(curProxy == null) {
FileProxy newProxy = new FileProxy(new File(uri));
FileProxy curProxy = map.putIfAbsent(newProxy.key, newProxy);
if(curProxy == null) {
curProxy = newProxy; // FileProxy was not in the map
}
}
try {
curProxy.mutex.acquire();
operator.operate(curProxy.file);
} finally {
curProxy.mutex.release();
}
}
}
The threads that are using a file implement FileOperator
or something similar. Files are hidden behind a FileProxy
that maintains a static ConcurrentHashMap
of key (URI, or absolute path, or some other file invariant) value (FileProxy
) pairs. Each FileProxy
maintains a Semaphore
that acts as a mutex - this is initialized with one permit. When the static operate
method is called, a new FileProxy
is created from the URI if none exists; the FileOperator
is then added to the FileProxy
queue; acquire
is called on the mutex to ensure that only one thread can operate on the file at a time; and finally the FileOperator
does its thing.
In this implementation, FileProxy
objects are never removed from the ConcurrentHashMap
- if this is a problem then a solution is to wrap the FileProxy
objects in a WeakReference
or SoftReference
so that they can be garbage collected, and then call map.replace
if reference.get() == null
to ensure that only one thread replaces the GC'd reference.