PseudoFileSemaphore (アプリケーションのインスタンス数を制御するには-その3)

菊池さんAWAWAさん、囚人さんらのアドバイスから以下の要件が導き出された。

  • ロックファイルを作るが、後始末として削除できなくても「そんなの関係ねぇ」
  • Java NIOのFileChannelのロックメカニズムを利用する
  • この場合はセマフォを使うべき

ということでFileChannelを利用して「システムセマフォもどき」を作ってみた。

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class PseudoFileSemaphore {
    private String name;
    private File file;
    private int permits;
    private FileLock filelock;
    private FileChannel channel; 

    public PseudoFileSemaphore(String name, int permits) {
        this.name = name;
        this.permits = permits;
    }
    public synchronized void release() {
        try {
            if ( this.filelock != null ) {
                this.filelock.release();
                this.filelock = null;
                
                this.channel.close();
                this.channel = null;
                this.file.delete();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public synchronized int tryAcquire()  {
        return tryAcquire(1);
    }
    public synchronized int tryAcquire(int requestPermits)  {
        try {
            if ( requestPermits > this.permits ) {
                throw new IllegalArgumentException("requestPermits too large");
            }
            FileChannel channel = this.createLockFileChannel();
            FileLock lock = null;
            int p;
            for ( p = 0; p < this.permits; p++ ) {
                try {
                    lock = channel.tryLock(p, requestPermits, false);
                    if ( lock != null 
                         && lock.isValid() 
                         && lock.position()+requestPermits <= channel.size()) {
                        break;
                    } else {
                        lock = null;
                    }
                } catch (OverlappingFileLockException e ) { 
                    continue;
                }
            }
            if ( lock != null ) {
                this.filelock = lock;
                this.channel = channel;
                return p + 1;
            } else {
                return 0;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return 0;
    }
    private FileChannel createLockFileChannel() throws IOException {
        this.file = new File(this.name+".lck");
        RandomAccessFile rFile = new RandomAccessFile(this.file, "rw");
        FileChannel channel = rFile.getChannel();
        rFile.setLength(this.permits);
        if ( rFile.length() < this.permits ) {
            rFile.seek(rFile.length());
            for (long i = 0; i < this.permits - rFile.length(); i++) {
                rFile.write(0xff);
            }
        } else {
            channel.truncate(this.permits);
        }
        return channel;
    }
}

FileChannelのロック機構はファイル中の任意の位置をロックできるので、コンストラクタセマフォの最大取得上限を決定したならばそれと同じ長さのファイルを作り、後はtryAcquireに対応したファイル位置に対してロックを試みる訳だ。ロックが成功すればパーミット取得成功でその後ロックはそのまま保持する。逆にロックに失敗したならば、すでに誰かが取得済みということでパーミット取得失敗とする。
パーミットの開放はreleaseメソッドで行う。このとき処理の最後でFile#deleteを実行しているが、実際にファイルが削除されるのは、最後のロックを解除した時のはずである。(他のロックが一つでも存在していればファイルが消えることは無い)

実際に使えることはテストで確認したが、例によって例外処理はいい加減なので、実際に使う場合は各自工夫のこと。

使い方は簡単。昨日の擬似コードに当てはめるならば、こんな感じで書ける。

PseudoFileSemaphore pSemaphore = new PseudoFileSemaphore(applicationId, 5); //5回同時起動可能
if ( pSemaphore.tryAcquire() != 0 ) {
    //起動OK!
} else {
    //起動NG!
}

通常であればアプリケーション終了時にreleaseメソッドを呼べば良いが、異常終了のことを考えてシャットダウンフックに登録しておくのもよいだろう。

Runtime.getRuntime().addShutdownHook(new Thread(new Runnable(){
    @Override
    public void run() {
        pSemaphore.release();
    }
}));

もちろんこのシャットダウンフックが実行されずにロックファイルが残ってしまっても次回のセマフォの獲得処理に問題は出ない。
今回はあまり排他制御の事は考えていないので、セマフォからのパーミット取得はノンブロッキングタイプのみとしたが、FileChannelを利用したロックはおあつらえ向きにロック時にブロッキングさせることもできるので、ブロックするacquireメソッドを作ることも可能だろう。

それにしてもFileChannelのロックが位置と長さを指定できることには全く頭が回らなかった。ファイルの排他ロックといえばファイルとロックは1:1の関係しかあり得ないと思っていた自分の頭の固さが情けない。

これでJavaでもシステム全体を通したアプリケーションインスタンス数の管理が実現できる運びとなった。
これも全てはアドバイスを頂いた方のお陰であり、本当に感謝する。

追記:
実際にGUIアプリケーションに組み込んでテストしてみたが快調だ。

追記:
パーミット数(ファイル長)が変えられたケースに対応するよう修正した
オーバラップする場合と現在のファイルの長さを超えてロックを作成するケースを除外するよう修正した。