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;
    }
}

如您所见,我没有使用同步块。我假设这里最坏的情况是竞争条件导致两个线程为同一个 userId 运行 cache.put(userId, photo)。但是两个线程的数据是相同的,所以这不是问题。

我的推理在这里正确吗?如果没有,有没有办法使用同步块而不会对性能造成很大影响?一次只有 1 个线程访问地图感觉有点矫枉过正。

4

2 回答 2

2

Assylias 是对的,您所拥有的一切都会正常工作。

但是,如果您想避免多次获取图像,这也是可能的,但需要做更多的工作。洞察力是,如果一个线程出现,缓存未命中并开始加载图像,那么如果第二个线程在第一个线程完成加载之前想要相同的图像,那么它应该等待第一个线程,而不是自己去加载它。

使用 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。并且由于此代码执行显式锁定,因此您可以从中删除synchronizedMap。您还可以将 aConcurrentMap用于缓存,这将允许使用 ,这putIfAbsent是 lock/get/check for null/put/unlock 序列的更多并发替代方案。

希望这里发生的事情是相当明显的。从缓存中获取东西的基本模式,检查你得到的东西是否为空,如果是的话,把东西放回去仍然存在。但不是放入 a Photo,而是放入 a Future,它本质上是 a 的占位符,Photo当时可能不(或可能)在那里,但稍后将可用。geton 方法获取一个地方所持有的东西,Future如果需要,阻塞直到它到达。

此代码FutureTask用作Future; this 接受Callable能够产生 aPhoto作为构造函数参数,并在调用它的run方法时调用它。调用run由一个测试保护,该测试基本上概括了if (photo == null)之前的测试,但在synchronized块之外(因为正如您所意识到的,您真的不希望在持有缓存锁的同时加载照片)。

这是我见过或需要几次的模式。遗憾的是它没有内置在某个地方的标准库中。

于 2013-02-13T20:28:26.037 回答
1

是的,你是对的——如果照片创建是幂等的(总是返回同一张照片),最糟糕的情况是你将多次获取它并将其多次放入地图中。

于 2013-02-13T09:25:06.510 回答