アプリケーションのインスタンス数を制御するには

Javaクライアントアプリケーションを書く私をずっと悩ませ続けている問題がある。それは、「いかにしてアプリケーションのインスタンス数(同時起動数)を制御(制限)するか」ということだ。

.NETプラットホームの場合であればプロセス名=アプリケーションを識別する名前という前提であれば、この問題は簡単に解決が可能だ。
具体的にはProcessクラスを列挙し、現在のプロセスと同じ名前のプロセスがあれば、同一のアプリケーションが存在している、と考えることができる。

public static Process GetProcessesForCurrent()
{
    return GetProcessesForName(Process.GetCurrentProcess().ProcessName);
}
public static Process GetProcessesForName(string processName)
{
    return Array.FindAll(Process.GetProcesses(),
        delegate(Process process)
        {
            return (process.ProcessName == processName);
        });
}
if ( GetProcessesForCurrent().Length + 1 <= app.LaunchLimit)
{
    //起動OK
}

通常、戻った配列の要素数=アプリケーションのインスタンス数と考えることができるのでその数が上限に到達していればそれ以上の起動を許可しなければ良い訳だ。

さて、Javaの場合はどうするか。(Javaにも同名のProcessクラスはあるが、残念ながら実行中のプロセスを列挙する機能などは持ち合わせていない。)

  • ロックファイル

昔からある技法だが、アプリケーションが起動したのと同時にファイルを作ってロックしておき、そのファイルをロックできなければアプリケーションが起動済みとするやり方である。

File lockFle = new File("lock.tmp");
if (lockFle.createNewFile()){
    //起動OK
}

この方法はシンプルだが問題がある。一つはアプリケーションのインスタンスを一つに制限することはできても、複数のインスタンス、例えば「10の同時起動を許可する」などの場合には使えないこと。もう一つはロックファイルの後始末を確実に行う術が無いということだ。候補としてはJVMのシャットダウンフックに登録したスレッドで行うことだが、これとて100%確実ではない
なんらかの原因で残ってしまったロックファイルをユーザに消すことを強制するのは最悪である。

  • データベースやプリファレンス等に現在のインスタンス数を記録しておく

これもよくある方法。データベースが必須のシステムであればそこに、そうでなければJDK1.4から追加されたPreferenceストアに現在のアプリケーションの起動回数を記録しておく方法。
この方法であれば制御するアプリケーションのインスタンスの数は如何様にもできるが、やはり後始末の問題は残る。(アプリケーションが落ちる際にカウント数を減じないと矛盾が生じる)

Preferences prefs = Preferences.userNodeForPackage(applcation.getClass());
int times = pref.node(applcation.getId()).getInt("launchTimes", 0);
if ( times + 1 <= applcation.getLaunchLimit() ) {
    //起動OK
    times++;
    pref.node(applcation.getId()).putInt("launchTimes", times);
} 
  • サーバソケットを使う

これは最近読んだ本「Swing Hacks」に掲載されていた、ちょっと面白い方法である。
Java Swing Hacks ―今日から使える驚きのGUIプログラミング集 Java Swing Hacks
方法はサーバソケットがバインド済みのポートを使って生成できないことを利用するやり方である。以下、本に掲載されているコードとは全く違うが、仕組みの考え方は同じだ。

final int DEF_PORT = 38976;
ServerSocket socket;
for ( int port = 0; port < app.getLaunchLimit(); port++ ) {
    try {
        socket = new ServerSocket(DEF_PORT + port);
        break;
    } catch (IOException e) {
        socket = null;
    }
}
if ( socket != null ) {
    //起動OK
}

この方法はコード非常にシンプルになるのと、上の二つとは違い明示的な後始末が仮にできなかったとしても実害が出にくいのが優れているが、アプリケーション毎に空きポート番号を振らなくてはならないこと、何より本来の目的とは違う目的でシステムの有限リソースを使用する後ろめたさが付きまとう。ネットワーク管理ポリシによってはこのような目的に貴重なポートを食わせるのは許可されないだろう。

という訳で、Javaの場合は未だに決定的な解決策を得ていない。