Java 在多线程环境中使用缓存映射安全吗?

Java 在多线程环境中使用缓存映射安全吗?,java,multithreading,caching,concurrency,lru,Java,Multithreading,Caching,Concurrency,Lru,我正在使用Commons Collections LRUMap(基本上是一个经过小修改的LinkedHashMap)为用户的照片实现一个LRU缓存。findPhoto方法可以在几秒钟内调用数百次 public class CacheHandler { private static final int MAX_ENTRIES = 1000; private static Map<Long, Photo> photoCache = Collections.synchron

我正在使用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;
    }
}
公共类缓存处理程序{
私有静态最终int MAX_条目=1000;
私有静态映射photoCache=Collections.synchronizedMap(新的LRUMap(MAX_条目));
公共静态映射getPhotoCache(){
返回光刻;
}
}
用法:

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;
    }
}
公共照片查找照片(长用户ID){
User=userDAO.find(userId);
如果(用户!=null){
Map cache=CacheHandler.getPhotoCache();
Photo Photo=cache.get(userId);
如果(照片==null){
if(user.isFromAD()){
试一试{
photo=LDAPService.getInstance().getPhoto(user.getLogin());
}捕获(LDAPSearchException e){
抛出新的EJBException(e);
}
}否则{
debug(“为外部用户从数据库获取照片:+user.getLogin());
UserFile file=userDAO.findUserFile(user.getPhotoId());
如果(文件!=null){
照片=新照片(file.getFilename(),“image/png”,file.getFileData());
}
}
cache.put(用户ID、照片);
}否则{
debug(“从缓存中获取照片,用户:+user.getLogin());
}
返回照片;
}否则{
返回null;
}
}
如您所见,我没有使用同步块。我假设这里最坏的情况是导致两个线程为同一个userId运行cache.put(userId,photo)的争用条件。但是两个线程的数据是相同的,所以这不是问题


我的推理正确吗?如果没有,是否有一种方法可以使用同步块而不会对性能造成很大影响?一次只有一个线程访问地图感觉太过分了。

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

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;
}
请注意,您需要更改CacheHandler.photoCache的类型以适应包装
FutureTask
s。由于此代码执行显式锁定,因此可以从中删除
synchronizedMap
。您还可以对缓存使用
ConcurrentMap
,这将允许使用比锁定/获取/检查null/put/unlock序列更并发的替代方法

希望这里发生的事情是相当明显的。从缓存中获取内容、检查获取的内容是否为空以及是否为空的基本模式仍然存在。但是你没有放一张
照片
,而是放了一张,它本质上是一张
照片的占位符
,这张照片当时可能不在(或者可能不在)那里,但以后会出现。
Future
上的
get
方法获取某个位置所要保留的对象,如有必要,将其阻塞直到它到达

此代码用作
未来
的实现;它将一个能够生成
Photo
的函数作为构造函数参数,并在调用其
run
方法时调用它。对
run
的调用受到一个测试的保护,该测试基本上重述了先前的
if(photo==null)
测试,但在
synchronized
块之外(因为正如您所意识到的,您确实不希望在保持缓存锁的同时加载照片)


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

谢谢你的想法,非常有教育意义。
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();
}