我正在使用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)。但是两个线程的数据是相同的,所以这不是问题。
我的推理在这里是否正确?如果没有,有没有办法使用同步块而不会获得大的性能影响?一次只有一个线程访问地图感觉有点矫枉过正。
答案 0 :(得分: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
的类型以适应包裹FutureTask
。由于此代码执行显式锁定,因此您可以从中删除synchronizedMap
。您还可以使用ConcurrentMap
作为缓存,这将允许使用putIfAbsent
,这是对/ null / put / unlock序列的锁定/获取/检查的更多并发替代。
希望这里发生的事情是相当明显的。从缓存中获取内容的基本模式,检查您获得的内容是否为null,如果是这样,那么将某些内容放回去仍然存在。但是,您输入Future
而不是放入Photo
,而FutureTask
实际上是Photo
的占位符,当时可能没有(或可能)在那里,但是将在稍后提供。 get
Future
上的Future
方法获取了一个地方被占用的东西,阻塞直到它必要时到达。
此代码使用Callable
作为Photo
的实现;这需要{{3}}能够生成run
作为构造函数参数,并在调用其run
方法时调用它。对if (photo == null)
的调用是通过一个测试来保护的,该测试基本上概括了synchronized
测试,但是在{{1}}块之外(因为你意识到,你真的不想加载拿着缓存锁的照片。)
这是我见过或需要几次的模式。遗憾的是它并没有内置在标准库中。
答案 1 :(得分:1)
是的你是对的 - 如果照片创作是幂等的(总是返回相同的照片),最糟糕的事情就是你会多次获取它并将它多次放入地图。