Javaにおけるファイルロックについて

いわゆるファイルロック

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

while (lockFile.createNewFile()) {
    try {
        // ロックを取得している場合の処理
        ...
    } finally {
        lockFile.delete();
    }
} else {
    throw new RuntimeException("ロックを取得できませんでした.");
}

上記はいわゆるファイルロックと言われるものです。
マルチスレッド環境の際に同じ処理が並列に実行されるとマズい場合、ファイルロックにより同じ処理が並列に実行され内容に制御します。
File#createNewFile() は、ファイルが存在しなければ新規にファイルを作成しTrueを返し、既にファイルが存在すればFalseを返します。
ただし、上記はうまく動かない場合があります。
詳しくはこちらを見ていただくといいかと思いますが、要はJavaVMが異常終了したときなどにロックファイルが残り続けてしまうのが問題となります。
ちなみに、

  • File#deleteOnExit()
    • JavaVMが「正常終了」した時にファイルを削除
    • 「正常終了」の定義は、Java言語仕様に書いてあるそうですが、プロセスをkillするとかディスクがぶっ飛ぶとかしない限りは正常終了に当たるっぽいです。(未確認)

FileLockオブジェクト

ということで紹介されている通り、FileLockオブジェクトを使用します。
ちなみにFile#createNewFile()のJavaDocにも、File#createNewFile()はファイルロックに使用すべきでなく、FileLockを使用するよう書いてあります。
FileLockを使用すると、下記のようになります。

try {
    File lockFile = new File("pid.lock");
    lockFile.deleteOnExit();

    fs = new FileInputStream(lockFile);
    FileChannel fc = fs.getChannel();

    FileLock lock = fc.tryLock();

    if (lock == null) {
        new RuntimeException("ロック中");
    }

    try {
        // ロックを獲得している場合の処理
        ...
    } finally {
        // ロックの開放
        lock.release();
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
    	fs.close();
    } catch (IOException e) {
    	e.printStackTrace();
    }
}

FileLockオブジェクトは、FileChannelを通して取得します。
FileLockは、ここに詳しい説明が書いてありますが、

基本となるオペレーティングシステムのネイティブのロック機能に直接マッピングされます。
このため、ファイルにアクセスできるあらゆるプログラムが、作成に使用された言語とは関係なく、このファイル上に保持されたロックを認識します。

だそうです。
FileLockオブジェクトは、たぶんJavaVMが正常終了・異常終了に関わらず、終了すれば消えてくれる??ので、File#createNewFile()みたいにロックが残りつづけることがないんでしょうかね。(未確認)
(File#createNewFile()は、ファイルの有無=ロックの有無なので。ということでFileLockオブジェクトを使用してもファイルは残る、でもロックは解除される??)

  • FileChannel
  • FileChannel#tryLock()
    • ロックを取得します。ただし、ロックを獲得できなかった場合は、すぐにメソッドを終了し、nullを返します。
    • 逆にFileChannel#lock()は、ロックを獲得するまでの間処理をブロックして待機します。
  • FileLock#release();
    • ロックを開放します。
    • FileChannelを先に閉じてしまうと、ClosedChannelExceptionが発生するので注意。
  • FileInputStream#close();
    • ファイル入力ストリームを閉じます。
    • ここにあるように、FileChannelは作成元となったストリームと密接に関係しているため、FileChannelも同時に閉じられます。

気になったこと

FileChannel#tryLock()の仕様

JavaDocによると、

オーバーラップしたロックが別のプログラムによって保持されていたためロックに失敗した場合、null が返されます。その他の原因でロックに失敗した場合は、適切な例外がスローされます。

とあります。
ですが、どうもnullが返されず、いつもOverlappingFileLockExceptionがスローされます。
nullっていつ返ってくるんでしょうかね。

ロックの種類

ここの余談にもありますが、Linuxはアドバイザリ・ロック、Windowsは強制ロックでJavaが実装されているようです。

  • アドバイザリ・ロック
    • あるプロセスのロックは異なるプロセスへ影響を与えない。
    • つまり異なるプロセス間のロックによる管理は行えない。
  • 強制ロック
    • あるプロセスのロックは異なるプロセスへも影響を与える。
    • つまり異なるプロセス間のロックも行える。

ということで、LinuxWindowsの挙動が違います。
Javaのプロセスの定義とスレッドの定義がイマイチまだよくわかっておらず、どうしたらマルチスレッドでどうしたらマルチプロセスなのか。
つまり、例えば java コマンドで複数実行すればマルチプロセスになるのかどうかとか。(←それくらいすぐ調べられるから調べろよ。。。)

FileLockの仕様

ここに下記のような文章があります。

ファイルロックは Java 仮想マシン全体のために保持されます。これらは、同一仮想マシン内の複数スレッドによるファイルへのアクセスを制御するには適していません。
ファイルロックオブジェクトは、複数の並行スレッドで安全に使用できます。

これがまた曲者で、読み手の読み方次第でいろんな解釈の仕方がありそうなんですが。。。
「複数スレッド」と「複数の並行スレッド」ってそんなに意味が変わらないような。
つまるところ、「ファイルアクセス制御」には使うな、でも「ロック」としては使えるってこと??

UbuntuJDK

Ubuntuを使用しているので、apt-getよりJDK1.6.10をインストールしています。
で、何が問題なのかと言うと、Ubuntuのコマンドには「fcntl(2)」コマンドがありません。。。
ここの余談では、LinuxJDKは「fcntl」で実装されているそうなんですが。
で、調べてみると「fcntl」はシステムコールだそうです。(システムコールとは)

システムコールの種類は unistd.h というファイルで一覧が確認できるみたいなので、調べてみました。
使ってるPCはcore2quad(アーキテクチャx86)なので、/usr/src/linux-headers-2.6.27-14-generic/include/asm-x86/unistd_32.h を見ると、

#define __NR_fcntl               55

って書いてあるので、システムコールとしては存在するみたいですね。
まあUbuntuLinuxという大きな枠組みの中の一つのカーネルですので、まああるとは思っていたのですがコマンドから使えないシステムコールもあるんですね。(まあそりゃそうか)
「55」っていう番号の意味はこことかがわかりやすいです。