@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.
Internally, this class normalizes the file's path to detect same physical files via equals().
Therefore, the client does not need to normalize the file's path when constructing a SynchronizedFile.
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 safely 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 where synchronization takes effect for threads
both within the same process and across different processes. |
static SynchronizedFile |
getInstanceWithSingleProcessLocking(java.io.File fileToSynchronize)
Returns a
SynchronizedFile instance where synchronization takes effect for threads
within the same process but not for threads across different processes. |
<V> V |
read(ExceptionFunction<java.io.File,V> action)
Executes an action that reads the file with a SHARED lock.
|
<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 where synchronization takes effect for threads
both within the same process and across different processes.
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. Note that currently it is not possible for the underlying
locking mechanism to delete these lock files. The client may delete the lock files only when
the locking mechanism is no longer in use.
This method will normalize the file's path first, so the client does not need to normalize the file's path in advance.
The file being synchronized may or may not already exist. However, in order for the lock file to be created, the parent directory of the file being synchronized must exist when this method is called.
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 existjava.lang.IllegalArgumentException - if the parent directory of file being synchronized does not
exist, or a regular file with the same path as the lock file accidentally exists next to
the file being synchronizedgetInstanceWithSingleProcessLocking(File)@NonNull public static SynchronizedFile getInstanceWithSingleProcessLocking(@NonNull java.io.File fileToSynchronize)
SynchronizedFile instance where synchronization takes effect for threads
within the same process but not for threads across different processes.
This method will normalize the file's path first, 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 should 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
action