Java: ファイルロック
Tagged:

仕事で某社のシステムを調べていたところ、Javaのファイルロックをこんなコードで実現してました。

    File lockFile = new File("file.lock");
    int retryCount = 30;  // timeout = 3sec
    while (! lockFile.createNewFile()) {
        if ((retryCount--) == 0) {
            throw new LockFailedException("give up!");
        }
        Thread.sleep(100);
    }
    try {
        // ファイルロックで保護された処理
        ...
    } finally {
        lockFile.delete();
    }
間違ったファイルロック実装

このコードのどこが問題なのでしょう?

ファイルロックにcreateNewFile()を使うと…

マジックナンバーを使っていたり副作用のある式を条件式に使っていたりと、 気になるところはいろいろあるかも知れませんが、このコードの一番の問題は "File.createNewFile() に頼ったロック機構になっている" ことです。どういうことか簡単に説明してみます。

まずは createNewFile() ですが、 これはファイルが存在しなければファイルを新規に作成するメソッドです。 新規にファイルが作成されれば true, 既にファイルが存在していれば false を返します。 さらにJavadocによれば『不可分 (atomic) に』とあります。 つまり複数のスレッド、複数のプロセスから同時にこのメソッドが実行されても、 どれか1つの呼出だけが正しく成功する、ということです。

前述のコードはこれを利用して、lockFile.createNewFile() が成功=ロックを取得、つまりファイルの有無をロックの有無としています。 その後はロックで保護された処理を try ブロックに記述し、 finally 節でファイルを削除=ロック解除としています。 排他制御で普通に使われるイディオムですね。問題なさそうです。

…なんてことはありません。 ロック用ファイルの削除までに Java VM が異常終了したらどうなるでしょうか? ファイルは削除されずにそのまま残ります。つまりロックは解除されません。 今回の実装では異常終了した VM とロック用ファイルの関連も分からないので、 復帰させるには、関係する全てのプロセスを止めてからロック用ファイルを削除するしかないのです。

New I/Oを使ったファイルロック

というわけで、Java でのファイルロックは New I/O の java.nio.channels.FileLock を使って以下のように実現できます。

    File lockFile = new File("file.lock");
    lockFile.deleteOnExit();

    FileOutputStream fs = new FileOutputStream(lockFile);
    try {
        FileChannel ch = fs.getChannel();
        FileLock lock = null;

        final long TIMEOUT = 3000L;  // timeout = 3sec
        final long WAIT = 100L;

        for (int i = 0; i < (TIMEOUT / WAIT); i++) {
            if (null != (lock = ch.tryLock())) {
                break;
            }
            Thread.sleep(WAIT);
        }
        if (null == lock) {
            throw new LockFailedException("give up!");
        }

        try {
            // ファイルロックで保護された処理
            ...
        } finally {
            lock.release();
        }
    } finally {
        fs.close();
    }
FileLock を使った実装

冒頭のコードと比べると若干、複雑になってますが、基本構造は同じなので難しくはないと思います。 この方法ならば先に述べた問題点をクリアできますが、 一点、同一 VM のスレッド間排他には使えないことには気を付けてください。 冒頭のコードではファイル有無=ロック有無でしたので、一応、スレッド間の排他も行えますが、 Java5 以前の FileLock 実装ではスレッド間(正確には異なるストリームから別々に取得した FileChannel による FileLock 間)の排他制御はプラットフォーム依存(Windows は排他 OK, Linux では排他 NG)となります。 (※ なお Java6 からはプラットフォーム依存性はなくなったようです。)

余談

前段でも少し触れましたが FileLock の挙動は基本的にプラットフォーム依存です。 ということで、具体的にどのような実装になっているか OpenJDK のソースを見てみました。

【 UNIX(Linux) 実装】

    fcntl((tryLock ? F_SETLKW64 : F_SETLK64),
          (shared ? F_RDLCK : F_WRLCK))

【 WinNT(Windows) 実装】

    LockFileEx((tryLock ? LOCKFILE_FAIL_IMMEDIATELY : 0) |
               (shared ? 0 : LOCKFILE_EXCLUSIVE_LOCK))

※ それぞれの説明はこちら ⇒ fcntl(2), LockFileEx

Linux と Windows との違いで大きいのは、やはりアドバイザリロックか強制ロックの違いでしょうか。 これは排他制御をアプリケーションレベル(アドバイザリロック)で行うか OS レベル(強制ロック)で行うの違いなのですが、よく理解していない人もいるようです。 冒頭のコードにも「Linux では FileLock が動かないので createNewFile を使った云々」という趣旨のコメントがありました。

あと LockFileEx の説明に『共有アクセスの目的でファイルの特定の領域をロックすると(共有ロック)、ロック側プロセスも含め、どのプロセスもその領域に書き込めなくなります。』とあるので、 プラットフォーム依存を考えた場合、ロック用ファイルとプロセス間で共有するファイルは分けておくのが無難なようです。

さらなる余談

そもそもファイル有無=ロック有無にするなんてことを考えるのは CGI 世代なのかなー?と思っていましたが、 Java 1.4.0 以前の createNewFile() の Javadoc に、こんな記述がありました。

This method, in combination with the deleteOnExit() method, can therefore serve as the basis for a simple but reliable cooperative file-locking protocol.
そのため、このメソッドは、deleteOnExit() メソッドと連携して、単純だが信頼できる連携式のファイルロックプロトコルの基礎となります。

この記述は Java 1.4.1 からは改められて (参照) ファイルロックには使うな、となったようです。 まあ、そもそも createNewFile() はシステムコールの open(O_CREAT|O_EXCL,0666) を使うので NFS とかじゃ使えないのですが。 (native実装はOpenJDKで確認しただけですが仕様からして大体はこれを使ってるはず)