1

Commons Collections LRUMap (基本的には LinkedHashMap に小さな変更を加えたもの) を使用して、ユーザーの写真用の LRU キャッシュを実装しています。findPhoto メソッドは、数秒以内に数百回呼び出すことができます。

public class CacheHandler {
    private static final int MAX_ENTRIES = 1000;
    private static Map<Long, Photo> photoCache = Collections.synchronizedMap(new LRUMap(MAX_ENTRIES));

    public static Map<Long, Photo> getPhotoCache() {
        return photoCache;
    }
}

使用法:

public Photo findPhoto(Long userId){
    User user = userDAO.find(userId);
    if (user != null) {
        Map<Long, Photo> cache = CacheHandler.getPhotoCache();

        Photo photo = cache.get(userId);
        if(photo == null){
            if (user.isFromAD()) {
                try {
                    photo = LDAPService.getInstance().getPhoto(user.getLogin());
                } catch (LDAPSearchException e) {
                    throw new EJBException(e);
                }
            } else {
                log.debug("Fetching photo from DB for external user: " + user.getLogin());
                UserFile file = userDAO.findUserFile(user.getPhotoId());
                if (file != null) {
                    photo = new Photo(file.getFilename(), "image/png", file.getFileData());
                }
            }
            cache.put(userId, photo);
        }else{
            log.debug("Fetching photo from cache, user: " + user.getLogin());
        }
        return photo;

    }else{
        return null;
    }
}

ご覧のとおり、同期ブロックは使用していません。ここでの最悪のシナリオは、2 つのスレッドが同じ userId に対して cache.put(userId, photo) を実行する原因となる競合状態であると想定しています。ただし、データは 2 つのスレッドで同じであるため、問題にはなりません。

ここでの私の推論は正しいですか?そうでない場合、パフォーマンスに大きな影響を与えずに同期ブロックを使用する方法はありますか? 一度に 1 つのスレッドだけがマップにアクセスするのはやり過ぎのように感じます。

4

2 に答える 2

2

Assylias は、あなたが持っているものがうまく機能するというのは正しいです。

ただし、画像を複数回フェッチすることを避けたい場合は、もう少し作業をすれば可能です。洞察としては、あるスレッドがキャッシュ ミスを起こし、画像の読み込みを開始した場合、最初のスレッドが読み込みを完了する前に 2 番目のスレッドが同じ画像を要求した場合、最初のスレッドを待機する必要があるということです。それ自体に行ってロードするのではなく。

これは、Java のより単純な並行性クラスのいくつかを使用して調整するのはかなり簡単です。

まず、あなたの例をリファクタリングして、興味深い部分を引き出してみましょう。ここにあなたが書いたものがあります:

public Photo findPhoto(User user) {
    Map<Long, Photo> cache = CacheHandler.getPhotoCache();

    Photo photo = cache.get(user.getId());
    if (photo == null) {
        photo = loadPhoto(user);
        cache.put(user.getId(), photo);
    }
    return photo;
}

ここでloadPhotoは、写真をロードする実際の核心を行う方法を示しますが、ここでは関係ありません。ユーザーの検証は、これを呼び出す別のメソッドで行われると想定しています。それ以外は、これがあなたのコードです。

代わりに行うことは次のとおりです。

public Photo findPhoto(final User user) throws InterruptedException, ExecutionException {
    Map<Long, Future<Photo>> cache = CacheHandler.getPhotoCache();

    Future<Photo> photo;
    FutureTask<Photo> task;

    synchronized (cache) {
        photo = cache.get(user.getId());
        if (photo == null) {
            task = new FutureTask<Photo>(new Callable<Photo>() {
                @Override
                public Photo call() throws Exception {
                    return loadPhoto(user);
                }
            });
            photo = task;
            cache.put(user.getId(), photo);
        }
        else {
            task = null;
        }
    }

    if (task != null) task.run();

    return photo.get();
}

CacheHandler.photoCacheラッピングFutureTasksに対応するには、 のタイプを変更する必要があることに注意してください。また、このコードは明示的なロックを行うため、 を削除できますsynchronizedMapConcurrentMapまた、キャッシュに を使用することもできます。これによりputIfAbsent、ヌル/プット/ロック解除シーケンスのロック/取得/チェックに代わる、より並行的な の使用が可能になります。

うまくいけば、ここで何が起こっているかは明らかです。キャッシュから何かを取得し、取得したものがnullかどうかを確認し、そうであれば何かを戻すという基本的なパターンはまだそこにあります。しかし、 を入れる代わりに を入れPhotoますFuture。これは本質的に、Photoその瞬間には存在しない (または存在する可能性がある) のプレースホルダーですが、後で利用可能になります。getメソッド onFutureは、場所が保持されているものを取得し、必要に応じて到着するまでブロックします。

このコードは;FutureTaskの実装として使用します。Futurethis はコンストラクタ引数として aCallableを生成できる を取り、そのメソッドが呼び出されたときにそれを呼び出します。への呼び出しは、基本的に以前のテストを再現するテストで保護されていますが、ブロックの外側にあります (お気づきのように、キャッシュ ロックを保持している間は写真をロードしたくないためです)。Photorunrunif (photo == null)synchronized

これは、私が何度か見たり必要としたりしたパターンです。標準ライブラリのどこかに組み込まれていないのは残念です。

于 2013-02-13T20:28:26.037 に答える