1. 概要

ファイルを読み書きするときは、適切なファイルロックメカニズムが設定されていることを確認する必要があります。 これにより、同時I/Oベースのアプリケーションでのデータの整合性が保証されます。

このチュートリアルでは、JavaNIOライブラリを使用してこれを実現するためのさまざまなアプローチを見ていきます。

2. ファイルロックの概要

一般に、ロックには2つのタイプがあります

    • 排他的ロック—書き込みロックとも呼ばれます
    • 共有ロック—読み取りロックとも呼ばれます

簡単に言うと、排他ロックは、書き込み操作の完了中に、読み取りを含む他のすべての操作を防止します。

対照的に、共有ロックを使用すると、複数のプロセスを同時に読み取ることができます。 読み取りロックのポイントは、別のプロセスによる書き込みロックの取得を防ぐことです。 通常、一貫性のある状態のファイルは、実際にどのプロセスでも読み取り可能である必要があります。

次のセクションでは、Javaがこれらのタイプのロックをどのように処理するかを見ていきます。

3. Javaでのファイルロック

Java NIOライブラリを使用すると、OSレベルでファイルをロックできます。 FileChannellock()および tryLock()メソッドはその目的のためのものです。

FileChannel は、 FileInputStream FileOutputStream 、またはRandomAccessFileのいずれかを介して作成できます。 3つすべてに、 FileChannelを返すgetChannel()メソッドがあります。

または、静的 open メソッドを使用して、FileChannelを直接作成することもできます。

try (FileChannel channel = FileChannel.open(path, openOptions)) {
  // write to the channel
}

次に、Javaで排他ロックと共有ロックを取得するためのさまざまなオプションを確認します。 ファイルチャネルの詳細については、Javaファイルチャネルガイドチュートリアルをご覧ください。

4. 排他的ロック

すでに学習したように、ファイルへの書き込み中に排他ロックを使用することで、他のプロセスによるファイルの読み取りまたは書き込みを防ぐことができます。

FileChannelクラスでlock()または tryLock()を呼び出すことにより、排他ロックを取得します。 オーバーロードされたメソッドを使用することもできます。

  • ロック(ロングポジション、ロングサイズ、ブール共有)
  • tryLock(ロングポジション、ロングサイズ、ブール値共有)

このような場合、sharedパラメーターをfalseに設定する必要があります。

排他ロックを取得するには、書き込み可能なFileChannelを使用する必要があります。 FileOutputStreamまたはRandomAccessFilegetChannel()メソッドを使用して作成できます。 または、前述のように、FileChannelクラスの静的openメソッドを使用できます。  必要なのは、2番目の引数をStandardOpenOption.APPENDに設定することだけです。

try (FileChannel channel = FileChannel.open(path, StandardOpenOption.APPEND)) { 
    // write to channel
}

4.1. FileOutputStreamを使用した排他的ロック

FileOutputStreamから作成されたFileChannelは書き込み可能です。 したがって、排他ロックを取得できます。

try (FileOutputStream fileOutputStream = new FileOutputStream("/tmp/testfile.txt");
     FileChannel channel = fileOutputStream.getChannel();
     FileLock lock = channel.lock()) { 
    // write to the channel
}

ここで、 channel.lock()は、ロックを取得するまでブロックするか、例外をスローします。 たとえば、指定された領域がすでにロックされている場合、OverlappingFileLockExceptionがスローされます。 考えられる例外の完全なリストについては、Javadocを参照してください。

channel.tryLock()を使用してノンブロッキングロックを実行することもできます。 別のプログラムが重複するロックを保持しているためにロックを取得できない場合は、nullを返します。 その他の理由で失敗した場合は、適切な例外がスローされます。

4.2. RandomAccessFileを使用した排他的ロック

RandomAccessFile を使用して、コンストラクターの2番目のパラメーターにフラグを設定する必要があります。

ここでは、読み取りと書き込みのアクセス許可でファイルを開きます。

try (RandomAccessFile file = new RandomAccessFile("/tmp/testfile.txt", "rw");
      FileChannel channel = file.getChannel();
      FileLock lock = channel.lock()) {
    // write to the channel
}

ファイルを読み取り専用モードで開いてそのチャネルに書き込もうとすると、NonWritableChannelExceptionがスローされます。

4.3. 排他的ロックには書き込み可能なFileChannelが必要です

前述のように、排他ロックには書き込み可能なチャネルが必要です。 したがって、FileInputStreamから作成されたFileChannelを介して排他ロックを取得することはできません。

Path path = Files.createTempFile("foo","txt");
Logger log = LoggerFactory.getLogger(this.getClass());
try (FileInputStream fis = new FileInputStream(path.toFile()); 
    FileLock lock = fis.getChannel().lock()) {
    // unreachable code
} catch (NonWritableChannelException e) {
    // handle exception
}

上記の例では、 lock()メソッドはNonWritableChannelExceptionをスローします。 実際、これは、読み取り専用チャネルを作成するFileInputStreamgetChannelを呼び出しているためです。

この例は、書き込み不可能なチャネルに書き込むことができないことを示すためだけのものです。 実際のシナリオでは、例外をキャッチして再スローすることはありません。

5. 共有ロック

共有ロックは読み取りロックとも呼ばれることを忘れないでください。 したがって、読み取りロックを取得するには、読み取り可能なFileChannelを使用する必要があります。

このようなFileChannelは、 FileInputStreamまたはRandomAccessFilegetChannel()メソッドを呼び出すことで取得できます。 ここでも、別のオプションは、FileChannelクラスの静的openメソッドを使用することです。 その場合、2番目の引数をStandardOpenOption.READに設定します。

try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
    FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
    // read from the channel
}

ここで注意すべきことの1つは、 lock(0、Long.MAX_VALUE、true)を呼び出してファイル全体をロックすることを選択したことです。 最初の2つのパラメーターを異なる値に変更することで、ファイルの特定の領域のみをロックすることもできます。 共有ロックの場合、3番目のパラメーターはtrueに設定する必要があります。

簡単にするために、以下のすべての例ではファイル全体をロックしますが、ファイルの特定の領域をいつでもロックできることに注意してください。

5.1. FileInputStreamを使用した共有ロック

FileInputStreamから取得したFileChannelは読み取り可能です。 したがって、共有ロックを取得できます。

try (FileInputStream fileInputStream = new FileInputStream("/tmp/testfile.txt");
    FileChannel channel = fileInputStream.getChannel();
    FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
    // read from the channel
}

上記のスニペットでは、チャネルでの lock()の呼び出しは成功します。 これは、共有ロックではチャネルが読み取り可能である必要があるだけだからです。 FileInputStream から作成したので、ここに当てはまります。

5.2. RandomAccessFileを使用した共有ロック

今回は、読み取り権限だけでファイルを開くことができます。

try (RandomAccessFile file = new RandomAccessFile("/tmp/testfile.txt", "r"); 
     FileChannel channel = file.getChannel();
     FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
     // read from the channel
}

この例では、読み取り権限を持つRandomAccessFileを作成しました。 そこから読み取り可能なチャネルを作成できるため、共有ロックを作成できます。

5.3. 共有ロックには読み取り可能なFileChannelが必要です

そのため、FileOutputStreamから作成されたFileChannelを介して共有ロックを取得することはできません。

Path path = Files.createTempFile("foo","txt");
try (FileOutputStream fis = new FileOutputStream(path.toFile()); 
    FileLock lock = fis.getChannel().lock(0, Long.MAX_VALUE, true)) {
    // unreachable code
} catch (NonWritableChannelException e) { 
    // handle exception
}

この例では、 lock()の呼び出しは、FileOutputStreamから作成されたチャネルの共有ロックを取得しようとします。 このようなチャネルは書き込み専用です。 チャネルが読み取り可能でなければならないという必要性を満たしていません。 これにより、NonWritableChannelExceptionがトリガーされます。

繰り返しになりますが、このスニペットは、読み取り不可能なチャネルから読み取ることができないことを示すためのものです。

6. 考慮事項

実際には、ファイルロックの使用は困難です。 ロック機構は持ち運びできません。 これを念頭に置いて、ロックロジックを作成する必要があります。

POSIXシステムでは、ロックは助言です。 特定のファイルの読み取りまたは書き込みを行うさまざまなプロセスは、ロックプロトコルについて合意する必要があります。 これにより、ファイルの整合性が保証されます。 OS自体はロックを強制しません。

Windowsでは、共有が許可されていない限り、ロックは排他的です。 OS固有のメカニズムの利点または欠点について説明することは、この記事の範囲外です。 ただし、ロックメカニズムを実装するときは、これらのニュアンスを知ることが重要です。

7. 結論

このチュートリアルでは、Javaでファイルロックを取得するためのいくつかの異なるオプションを確認しました。

まず、2つの主要なロックメカニズムと、JavaNIOライブラリがファイルのロックを容易にする方法を理解することから始めました。 次に、アプリケーションで排他ロックと共有ロックを取得できることを示す一連の簡単な例を見ていきました。 また、ファイルロックを操作するときに発生する可能性のある一般的な例外の種類についても見てきました。

いつものように、例のソースコードはGitHubから入手できます。