Java 在多线程环境中使用缓存映射安全吗?
我正在使用Commons Collections LRUMap(基本上是一个经过小修改的LinkedHashMap)为用户的照片实现一个LRU缓存。findPhoto方法可以在几秒钟内调用数百次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
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();
}