在多线程环境中这种缓存映射的使用是否安全?

时间:2013-02-13 08:01:07

标签: 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.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)。但是两个线程的数据是相同的,所以这不是问题。

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

2 个答案:

答案 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)

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