@Immutable
public final class SynchronizedFile
extends java.lang.Object
When multiple threads or processes access the same file, they would require some form of synchronization. This class provides a simple way for the clients to add synchronization capability to a file without having to work with low-level details involving single-process or multi-process locking.
Synchronization can take effect for threads within the same process or across different
processes. The client can configure this locking scope when constructing a SynchronizedFile. If the file is never accessed by more than one process at a time, the client
should configure the file with SINGLE_PROCESS locking scope since there will be less
synchronization overhead. However, if the file may be accessed by more than one process at a
time, the client must configure the file with MULTI_PROCESS locking scope.
In any case, synchronization takes effect only for the same file (i.e., threads/processes
accessing different files are not synchronized). Also, the client must access the file via SynchronizedFile's API; otherwise, the previous concurrency guarantees will not hold.
Two files are considered the same if they refer to the same physical file. There could be
multiple instances of SynchronizedFile for the same physical file, and as long as they
refer to the same physical file, access to them will be synchronized.
Once the SynchronizedFile is constructed, the client can read or write to the file as
follows.
boolean fileExists = synchronizedFile.read(file -> file.exists());
boolean result = synchronizedFile.write(file -> { Files.touch(file); return true; });
Multiple threads/processes can read the same file at the same time. However, once a
thread/process starts writing to the file, the other threads/processes will block. This behavior
is similar to a ReadWriteLock.
Additionally, this class provides the createIfAbsent(ExceptionConsumer) method for
the client to atomically create the file if it does not yet exist.
Note that we often use the term "process", although the term "JVM" would be more correct since there could exist multiple JVMs in a process.
This class is thread-safe.
| Modifier and Type | Method and Description |
|---|---|
void |
createIfAbsent(ExceptionConsumer<java.io.File> action)
Executes an action that creates the file if it does not yet exist.
|
static SynchronizedFile |
getInstanceWithMultiProcessLocking(java.io.File fileToSynchronize)
Returns a
SynchronizedFile instance for the given file, where synchronization on the
same file takes effect for threads both within the same process and across different
processes (two files are the same if they refer to the same physical file). |
static SynchronizedFile |
getInstanceWithSingleProcessLocking(java.io.File fileToSynchronize)
Returns a
SynchronizedFile instance for the given file, where synchronization on the
same file takes effect for threads within the same process but not for threads across
different processes (two files are the same if they refer to the same physical file). |
<V> V |
read(ExceptionFunction<java.io.File,V> action)
Executes an action that reads the file with a SHARED lock.
|
java.lang.String |
toString() |
<V> V |
write(ExceptionFunction<java.io.File,V> action)
Executes an action that writes to (or deletes) the file with an EXCLUSIVE lock.
|
@NonNull public static SynchronizedFile getInstanceWithMultiProcessLocking(@NonNull java.io.File fileToSynchronize)
SynchronizedFile instance for the given file, where synchronization on the
same file takes effect for threads both within the same process and across different
processes (two files are the same if they refer to the same physical file).
Inter-process synchronization is provided via ReadWriteProcessLock, which requires
a lock file to be created. This lock file is different from the file being synchronized and
will be placed next to that file under the same parent directory.
The file being synchronized and the lock file may or may not exist when this method is called. The lock file will be created if it does not yet exist and will not be deleted after this method is called.
In order for the lock file to be created (if it does not yet exist), the parent directory of the file being synchronized and the lock file must exist when this method is called.
IMPORTANT: The lock file must be used solely for synchronization purposes. The client of this class must not access (read, write, or delete) the lock file. The client may delete the lock file only when the locking mechanism is no longer in use.
This method will normalize the file's path first to detect same physical files via equals(), so the client does not need to normalize the file's path in advance.
Note: If the file is never accessed by more than one process at a time, the client should
use the getInstanceWithSingleProcessLocking(File) method instead since there will be
less synchronization overhead.
fileToSynchronize - the file whose access will be synchronized; it may not yet exist,
but its parent directory must existgetInstanceWithSingleProcessLocking(File)@NonNull public static SynchronizedFile getInstanceWithSingleProcessLocking(@NonNull java.io.File fileToSynchronize)
SynchronizedFile instance for the given file, where synchronization on the
same file takes effect for threads within the same process but not for threads across
different processes (two files are the same if they refer to the same physical file).
The file being synchronized may or may not exist when this method is called.
This method will normalize the file's path first to detect same physical files via equals(), so the client does not need to normalize the file's path in advance.
Note: If the file may be accessed by more than one process at a time, the client must use
the getInstanceWithMultiProcessLocking(File) method instead.
fileToSynchronize - the file whose access will be synchronized, which may not yet existgetInstanceWithMultiProcessLocking(File)public <V> V read(@NonNull
ExceptionFunction<java.io.File,V> action)
throws java.util.concurrent.ExecutionException
action - the action that will read the filejava.util.concurrent.ExecutionException - if an exception occurred during the execution of the actionjava.lang.RuntimeException - if a runtime exception occurred, but not during the execution of the
actionpublic <V> V write(@NonNull
ExceptionFunction<java.io.File,V> action)
throws java.util.concurrent.ExecutionException
action - the action that will write to (or delete) the filejava.util.concurrent.ExecutionException - if an exception occurred during the execution of the actionjava.lang.RuntimeException - if a runtime exception occurred, but not during the execution of the
actionpublic void createIfAbsent(@NonNull
ExceptionConsumer<java.io.File> action)
throws java.util.concurrent.ExecutionException
This method throws a RuntimeException if the file does not exist but the action
does not create the file.
WARNING: It is not guaranteed that the file must exist after this method is executed, since another thread/process might delete it after this method returns (known as the TOCTTOU problem). Therefore, if a client wants to use this method to make sure the file exists for a subsequent action, it must also make sure that no intervening thread/process may be deleting the file after it is created.
action - the action that will create the filejava.util.concurrent.ExecutionException - if an exception occurred during the execution of the actionjava.lang.RuntimeException - if a runtime exception occurred, but not during the execution of the
actionpublic java.lang.String toString()
toString in class java.lang.Object