Как организовать потокобезопасную работу с файлами в Java?

«Как организовать потокобезопасную работу с файлами в Java?» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Потокобезопасный доступ к файлам в Java требует синхронизации операций записи и, в некоторых случаях, чтения. Встроенный монитор (synchronized) — базовый, но не всегда оптимальный механизм.

Способы синхронизации доступа к файлу:

  1. Использование synchronized (Intrinsic Lock):

    public class LogWriter {
        private final Object lock = new Object();
        public void writeEntry(String entry) {
            synchronized(lock) { // Только один поток пишет в файл
                try (FileWriter fw = new FileWriter("app.log", true)) {
                    fw.write(entry + "n");
                } catch (IOException e) { /* обработка ошибки */ }
            }
        }
    }
  2. Использование ReentrantReadWriteLock (для "много читателей, один писатель"):

    import java.util.concurrent.locks.ReentrantReadWriteLock;
    public class ThreadSafeFileCache {
        private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
        public String readFromFile() {
            rwLock.readLock().lock(); // Много потоков могут читать одновременно
            try {
                // ... чтение файла
            } finally { rwLock.readLock().unlock(); }
        }
        public void writeToFile(String data) {
            rwLock.writeLock().lock(); // Только один поток может писать
            try {
                // ... запись в файл
            } finally { rwLock.writeLock().unlock(); }
        }
    }
  3. Использование файловых блокировок ОС (java.nio.channels.FileLock):

    import java.nio.channels.FileChannel;
    import java.nio.file.StandardOpenOption;
    public class NioFileLockExample {
        public void writeWithLock(String data) throws IOException {
            try (FileChannel channel = FileChannel.open(
                    Paths.get("data.txt"),
                    StandardOpenOption.WRITE,
                    StandardOpenOption.CREATE)) {
                // Эксклюзивная блокировка файла (для записи)
                try (FileLock lock = channel.lock()) {
                    // Безопасная запись в файл
                    channel.write(ByteBuffer.wrap(data.getBytes()));
                } // Lock автоматически освобождается
            }
        }
    }

Рекомендации:

  • Для простых сценариев достаточно synchronized.
  • Если чтение происходит часто, а запись редко, используйте ReentrantReadWriteLock для повышения производительности.
  • FileLock полезен для координации между разными процессами (JVM), а не только потоками.
  • Рассмотрите возможность отказа от разделяемого файла — используйте очередь задач и один поток-писатель.