Javaで独自キャッシュを作る

よく業務でWebアプリを作るんですが、フレームワーク作ってたらクラスのメタ情報が欲しくなったりするじゃないですか。
もちろん毎回メタ情報を取りに行くのは無駄なわけで、みんなが至る結論はキャッシュを使うこと。

でもWebアプリだからマルチスレッドなわけで、キャッシュするにはsynchronizedをいろいろ使ってやんなきゃだったりするわけです。
でもsynchronizedは遅くなるって言われてるわけで、そんなときに参考になるのがこの本の5章に書かれてる内容です。
Java並行処理プログラミング ―その「基盤」と「最新API」を究める―
Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

一年以上前に読んだときは衝撃的だったなぁ。
5章の最後、「効率的でスケーラブルなリザルトキャッシュを構築する」(List5-19)に、最終的な答えとなるソースが出てきて、当時早速これを参考にして作りました。

上みたいに、クラスのメタ情報とかを取得したい場合はだいたい次のような感じ。
まずはキャッシュしたい内容を生成するクラスを作る。これは次のようなinitメソッドを持ったインターフェースを用意して、そいつを実装します。

public interface CacheInitializer<E>{
    void init(E type);
}

クラスを解析してキャッシュするので、こんなかんじで実装。ジェネリクスがちょっとめんどい。

public class BeanMetaData implements CacheInitializer<Class<?>> {
    private Object beanMetaInfo;
    public void init(Class<?> type) {
        //ここでBeanを解析して、インスタンスフィールドbeanMetaInfoに保持させる。
        this.beanMetaInfo=hogehoge;
    }
    public Object getMetaData(){
        //フィールドに保持していたデータを返す。
        return beanMetaInfo;
    }
}

initのパラメータでもらうクラスがメタ情報を解析する対象。解析したものはインスタンスのフィールドなんかで保持。

で、次にこいつらを保持しておく部分。

import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public abstract class CacheRepository<E, T extends CacheInitializer<E>> {
    private ConcurrentMap<E, Future<T>> futureCache = new ConcurrentHashMap<E, Future<T>>();

    public T getCache(final E target) throws InterruptedException {
        Future<T> future = futureCache.get(target);
        if (future == null) {
            Callable<T> eval = new Callable<T>() {
                public T call() throws Exception {
                    T info = newInitializerInstance();
                    info.init(target);
                    return info;
                }
            };
            FutureTask<T> futureTask = new FutureTask<T>(eval);
            future = futureCache.putIfAbsent(target, futureTask);
            if (future == null) {
                future = futureTask;
                futureTask.run();
            }
        }
        try {
            return future.get();
        } catch (CancellationException e) {
            futureCache.remove(target, future);
            throw launderThrowable(e);
        } catch (ExecutionException e) {
            throw launderThrowable(e.getCause());
        }
    }

    public static RuntimeException launderThrowable(Throwable t) {
        if (t instanceof RuntimeException) {
            return (RuntimeException) t;
        } else if (t instanceof Error) {
            throw (Error) t;
        } else {
            throw new IllegalStateException("Not unchecked", t);
        }
    }

    protected abstract T newInitializerInstance();
}

実際使うときは継承して使う。Singletonで作ったりしてインスタンスを1つにする。ここではやってないけどね。

public class BeanMetaRepository extends CacheRepository<Class<?>, BeanMetaData> {
    @Override
    protected BeanMetaData newInitializerInstance() {
        return new BeanMetaData();
    }
}

で、実際使うときはこんな感じ。ジェネリクスによってgetCacheのパラメータはClassになる。

public class Hoge {
    public static void main(String[] args) {
        BeanMetaRepository repo = new BeanMetaRepository();
        try {
            Object o = repo.getCache(Hoge.class).getMetaData();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

1年以上使ってるのですが いまだに疑問なのが、普通にsynchronizedをした場合に比べて いったいどれくらい早くなるんだろうかということ。一度、キャッシュを利用しない場合と比べたことがあるんですが、そのときは当然大きな違いが出ました。でもこれは問題外。
たぶん、ほとんど影響にならない程度じゃないかとおもわれ。StrutsでもActionのインスタンス管理はsynchronizedブロックでやってるし。

それと、独自キャッシュとか作ってメタ情報管理しちゃうと、最近のDIコンテナとは非常に相性がわるい。Singletonはなんとかなるんですが、インスタンスをPrototypeで作ってAOPとかしちゃった場合には完全にOUTですからね。