2

I'm trying to write to a file, using a Java FileLock, to prohibit all other processes and threads from reading from it or writing to it until I'm finished with it. Given this question and the answers to it, it seems to me like this is the perfect tool for what I want--a mutex for file access.

However, I am very concerned about this text from the JavaDocs:

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.

Can someone either alleviate my fears or point me in the right direction? It sounds like FileLock won't work at all to keep a different thread out of the file, even if another thread has already obtained it. If this is the case, is there another canonical Java method to do this that will protect from other threads?

Community
  • 1
  • 1
kmort
  • 2,848
  • 2
  • 32
  • 54
  • 1
    Use normal mutual exclusion methods (`synchronized` blocks or `ReentrantLocks`) to keep threads from accessing the same resources at the same time. – Solomon Slow May 19 '15 at 21:12

2 Answers2

4

The FileLock is a process level lock and will thus not protect the file from concurrent access from multiple threads within the process that has the lock.

You need to use a combination of the FileLock to protect from concurrent access from other processes and some other synchronization mechanism (like a synchronized method for accessing the file) within your process to protect from concurrent access by your own threads.

K Erlandsson
  • 13,408
  • 6
  • 51
  • 67
2

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.

Zim-Zam O'Pootertoot
  • 17,888
  • 4
  • 41
  • 69