Thread-Specific Storageパターン

結城浩

目次

Thread-Specific Storageパターン

Thread-Specific Storage(スレッド・スペシフィック・ストレージ)パターンのお話をはじめます。

次のような、インタフェースがあったとします。 このLegacySystemインタフェースでは、callメソッドを実行した後、 エラーが起きたかどうか、どういうエラーが起きたかを調べたかったら、 errnoメソッドで調べるものとします (この前提はいささか間の抜けた話のように聞こえるかもしれませんが、 現実のシステムではよくある話です)。

interface LegacySystem {
    public void call(int parameter);
    public int errno();
}

例えば、次のようなLegacySystemImplクラスが、 LegacySystemインタフェースを実装したとします。 ここでは単純のため、callの実際の仕事は何もなく、 単に引数のparameterをエラー(errno)に見立てています。

class LegacySystemImpl implements LegacySystem {
    private int errno;
    public void call(int parameter) { errno = parameter; }
    public int errno() { return errno; }
}

シングルスレッドのアプリケーションなら、 このLegacySystemImplクラスを使うことに特に問題はありません。 でも、このLegacySystemImplクラスのインスタンスが、 複数のスレッドからアクセスされたら、問題が発生します。 要するに、他のスレッドの呼び出したcallメソッドが、 自分の呼び出したerrnoメソッドの結果に影響してしまうのです。

実際に試してみましょう。

次のMain1は2つのスレッドからLegacySystemImplを利用します。 最初のスレッドはcall(0)を呼びつづけます。 5秒たってから次のスレッドが起動し、call(1)を呼びつづけます。 もしも、自分がcallに与えた値とerrnoメソッドの戻り値が異なったら、???を表示して、 System.exitで終了します。

class Main1 extends Thread {
    private static LegacySystem system = new LegacySystemImpl();
    private int value = 0;
    public Main1(int value) {
        this.value = value;
    }
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " checks.");
            system.call(value);
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            int errno = system.errno();
            System.out.println(Thread.currentThread().getName() + ": value = " + value + ", errno = " + errno);
            if (value != errno) {
                System.out.println("???");
                System.exit(0);
            }
        }
    }
    public static void main(String[] args) {
        new Main1(0).start();
        try { Thread.sleep(5000); } catch (InterruptedException e) {}
        new Main1(1).start();
    }
}

さあ、動かしてみましょう。次のように、???が出て終了します。

Thread-0 checks.
Thread-0: value = 0, errno = 0
Thread-0 checks.
Thread-0: value = 0, errno = 0
Thread-0 checks.
Thread-0: value = 0, errno = 0
(中略)
Thread-0 checks.
Thread-0: value = 0, errno = 0
Thread-0 checks.
Thread-1 checks.                        ←Thread-1が動き始めると…
Thread-0: value = 0, errno = 1          ←結果がおかしくなってしまった。
???

さてここで、 次のようなProxyを考えます。

class LegacySystemProxy implements LegacySystem {
    private ThreadLocal thlocal = new ThreadLocal();
    public void call(int parameter) {
        getImpl().call(parameter);
    }
    public int errno() {
        return getImpl().errno();
    }
    private LegacySystemImpl getImpl() {
        LegacySystemImpl impl = (LegacySystemImpl)thlocal.get();
        if (impl == null) {
            impl = new LegacySystemImpl();
            thlocal.set(impl);
        }
        return impl;
    }
}

ここで使われているjava.lang.ThreadLocalは、 現在のスレッドに固有な領域(threadに対してspecificな領域)を確保するクラスです。 ここでは、スレッドに固有なLegacySystemImplたちを 保持するためにthlocalフィールドが使われています。

ThreadLocalのgetメソッドとsetメソッドによって、 現在のスレッドに固有な領域への読み書きができます。

それでは、新しいMain2を使って、LegacySystemProxyを利用してみましょう。

class Main2 extends Thread {
    private static LegacySystem system = new LegacySystemProxy();
    private int value = 0;
    public Main2(int value) {
        this.value = value;
    }
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " checks.");
            system.call(value);
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            int errno = system.errno();
            System.out.println(Thread.currentThread().getName() + ": value = " + value + ", errno = " + errno);
            if (value != errno) {
                System.out.println("???");
                System.exit(0);
            }
        }
    }
    public static void main(String[] args) {
        new Main2(0).start();
        try { Thread.sleep(5000); } catch (InterruptedException e) {}
        new Main2(1).start();
    }
}

実行結果はどうなるでしょう。 わくわく。

Thread-0 checks.
Thread-0: value = 0, errno = 0
(中略)
Thread-0 checks.
Thread-1 checks.
Thread-0: value = 0, errno = 0
Thread-0 checks.
Thread-1: value = 1, errno = 1
Thread-1 checks.
Thread-0: value = 0, errno = 0
Thread-0 checks.                    ←このように
Thread-1 checks.                    ←ダブっても大丈夫。
Thread-0: value = 0, errno = 0
Thread-0 checks.
Thread-1: value = 1, errno = 1
Thread-1 checks.
Thread-0: value = 0, errno = 0
(後略)

このパターンは、以下の書籍(p.475)に書かれています。 解説の文章およびプログラムは結城が新たに書き起こしました。

このパターンは、以下の書籍でも詳しく解説します。

読者からの反応

読者さんから以下のコメントをいただきましたのでご紹介します。

読者さんから(2005-03-17)

他の人のコードを読んでいて TheadLocal が何者かよく分からなかったので、Googったらこのページに来ました。 非常に分かりやすい例があり、モヤモヤがスッキリした気がします。

LegacySystemProxy クラスの getImpl() メソッドですが、LegacySystemImpl を返すべきなのでしょうか? インターフェースで十分な気がします。 実装クラスを返すべきとする理由が何かありますか?

private LegacySystem getImpl() {
    LegacySystem impl = (LegacySystem)thlocal.get();
    if (impl == null) {
        impl = new LegacySystemImpl();
        thlocal.set(impl);
    }
    return impl;
}

結城から

フィードバックありがとうございます。 そうですね。ご指摘のように、インタフェースを返すほうがよいと思います。

なお、JDK 1.5以降では、 Genericsを使って次のようにしたほうが意味がはっきりしますし、 キャストも不要になります。

class LegacySystemProxy implements LegacySystem {
    private ThreadLocal<LegacySystem> thlocal = new ThreadLocal<LegacySystem>();
    public void call(int parameter) {
        getImpl().call(parameter);
    }
    public int errno() {
        return getImpl().errno();
    }
    private LegacySystem getImpl() {
        LegacySystem impl = thlocal.get();
        if (impl == null) {
            impl = new LegacySystemImpl();
            thlocal.set(impl);
        }
        return impl;
    }
}

読者さんから(2005-01-14)

ThreadLocalを実務で使おうと検索していてこのページを見つけました。このサンプルについて意見があります。

1、Main1とMain2では仕様が違うことが分かりにくい
  Main1ではLegacySystemImplは複数スレッドで1つを共有してますが、
  Main2ではスレッド毎に生成されるので複数になる

2、もしスレッド毎にLegacySystemImplを生成して良いなら
  Main1の以下のコードからstaticを除くのが良いでしょう。
  private static LegacySystem system = new LegacySystemImpl();

勝手なことを書いて、すいません。
ただ結城さんにはこの業界のスキルアップに貢献されているので重箱の隅をつつくような書き込みをしてしまいました。

ちなみに私が今悩んでいるのは、
サーバサイドのロジック内で使われるstaticメソッド(ログ出力など)でThreadLocalを使うことです。
目的はログにユーザIDを出力したいが、全メソッドにユーザIDを引きずりまわしたくない。
更にJUnitを使うのでセッションからユーザIDを取り出したくない。という問題です。

結城から

コメントありがとうございます。 最後の「悩み」に対して、 たとえばAspect指向な解決策(AOP)は適用できないでしょうか。 思い付きですけれど。

ぜひ、感想をお送りください

あなたのご意見・感想をお送りください。 あなたの一言が大きなはげみとなりますので、どんなことでもどうぞ。

あなたの名前: メール:
学年・職業など: 年齢: 男性女性
(上の情報は、いずれも未記入でかまいません)

お手数ですが、以下の問いに答えてから送信してください(迷惑書き込み防止のため)。
今年は西暦何年ですか?

何かの理由でうまく送れない場合にはメールhyuki dot mail at hyuki dot comあてにお願いします。

更新履歴

豊かな人生のための四つの法則