Thread Safe な汎用オブジェクトCache

  • 「DB負荷を減らしたいのでマスタテーブルはキャッシュして下さい><」だとか
  • 「ファイルIOは重いのでテンプレートはキャッシュして下さい><」だとか

割と良く言われたりします。

オブジェクトのキャッシュは手軽な高速化の手段だと思われているようですが、並列性に気を使う必要性があったり、キャッシュされたオブジェクトを使用する側でも注意深く扱う必要が発生したりと、何かと不具合の温床になるので、大抵の場合なるべく別の方法を提案します。

それでも最終的にキャッシュした方がいい、という結論になるときもままあるので、Thread Safe で汎用的なCacheクラスを作成しました。

import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicReference;

public class Cache<T> {
    private Callable<T> originalFactory;
    private AtomicReference<Future<T>> cache = new AtomicReference<Future<T>>();
    public Cache(Callable<T> originalFactory) {
        this.originalFactory = originalFactory;
    }

    public T get() {
        try {
            return getFuture().get();
        } catch (ExecutionException e) {
            // ここの処理は現場のポリシーによって柔軟に
            throw new RuntimeException(e.getCause());
        } catch (InterruptedException e) {
            throw new CancellationException(e.getMessage());
        }
    }

    private Future<T> getFuture() {
        Future<T> result = cache.get();
        if (result != null) return result;
        FutureTask<T> task = new FutureTask<T>(originalFactory);
        if (cache.compareAndSet(null, task)) {
            task.run();
        }
        return cache.get();
    }
}

使い方としては、実際のオブジェクトを作成する Factory として Callableを渡します。
オブジェクトを使用したい時にget()を行うだけです。

サンプル

@Mutable
public class Employee implements Cloneable {
    private int id;
    private String name;
    private Date birthday;
    // accessor 省略
    public Employee clone() {
        // 何らかの方法で Deep Copy
    }
}

public interface EmployeeFinder {
    List<Employee> findAll();
}

public class CachingEmployeeFinder  implements EmployeeFinder {
    
    private EmployeeFinder originalFinder;
    private Callable<List<Employee>> factory = new Callable<List<Employee>>() {
        public List<Employee> call() {
            return originalFinder.findAll();
        }
    };
    private Cache<List<Employee>> cache = new Cache<List<Employee>>(factory);

    public List<Employee> findAll() {
        List<Employee> original = cache.get();
        List<Employee> result = new ArrayList<Employee>(original.size());
        for (Employee employee : original) {
            result.add(employee.clone());
        }
        return result;
    }
    
    public void setOriginalFinder(EmployeeFinder originalFinder) {
        this.originalFinder = originalFinder;
    }

}

この場合、キャッシュしたいのは Employee の List ですね。

注意しなければならないのは、Cache を使えばそれだけで安心という訳にはいかない点です。キャッシュしているオブジェクトが Mutable だった場合、同じ参照をそのまま返すと、使用者が値変更なんてしてしまった場合に恐ろしい目にあいます。

なので大抵の場合コピーを返すことになります。また、さらに内部に Mutable なオブジェクトを保持している場合、Deep Copy をしてやる必要性があります。

ここまでしてやっと安全に使用することができます。

こうした諸々を行った結果、DBアクセスよりも Deep Copy の方がコストが大きかったなんて事もあったりするので笑えません。

オブジェクトをキャッシュする時は重々検討をした上で行って下さい。