Class SynchronizedFile

java.lang.Object
com.android.builder.utils.SynchronizedFile

@Immutable public final class SynchronizedFile extends Object
Utility to synchronize access to a file from multiple threads or processes.

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.

  • Method Details

    • getInstanceWithMultiProcessLocking

      @NonNull public static SynchronizedFile getInstanceWithMultiProcessLocking(@NonNull 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).

      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.

      Parameters:
      fileToSynchronize - the file whose access will be synchronized; it may not yet exist, but its parent directory must exist
      See Also:
    • getInstanceWithSingleProcessLocking

      @NonNull public static SynchronizedFile getInstanceWithSingleProcessLocking(@NonNull 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).

      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.

      Parameters:
      fileToSynchronize - the file whose access will be synchronized, which may not yet exist
      See Also:
    • getLockFile

      @NonNull public static File getLockFile(@NonNull File fileToSynchronize)
      Returns the path to the lock file that has been or will be created next to the file being synchronized under the same parent directory.
      Parameters:
      fileToSynchronize - the file whose access is synchronized, which may not yet exist
      Returns:
      the lock file, which may not yet exist
    • read

      public <V> V read(@NonNull ExceptionFunction<File,V> action) throws ExecutionException
      Executes an action that reads the file with a SHARED lock.
      Parameters:
      action - the action that will read the file
      Returns:
      the result of the action
      Throws:
      ExecutionException - if an exception occurred during the execution of the action
      RuntimeException - if a runtime exception occurred, but not during the execution of the action
    • write

      public <V> V write(@NonNull ExceptionFunction<File,V> action) throws ExecutionException
      Executes an action that writes to (or deletes) the file with an EXCLUSIVE lock.
      Parameters:
      action - the action that will write to (or delete) the file
      Returns:
      the result of the action
      Throws:
      ExecutionException - if an exception occurred during the execution of the action
      RuntimeException - if a runtime exception occurred, but not during the execution of the action
    • createIfAbsent

      public void createIfAbsent(@NonNull ExceptionConsumer<File> action) throws ExecutionException
      Executes an action that creates the file if it does not yet exist. This method is performed atomically.

      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.

      Parameters:
      action - the action that will create the file
      Throws:
      ExecutionException - if an exception occurred during the execution of the action
      RuntimeException - if a runtime exception occurred, but not during the execution of the action
    • toString

      public String toString()
      Overrides:
      toString in class Object