2013-02-13 62 views
0

我正在爲用戶的照片實現一個LRU緩存,使用Commons Collections LRUMap(基本上是一個帶有小修改的LinkedHashMap)。 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

是的你是對的 - 如果照片創作是冪等的(總是返回相同的照片),最糟糕的情況是你將不止一次地獲取它並將其放入地圖中多次。

1

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,如果是,則返回內容仍然存在。但不是放入Photo,而是放入Future,它基本上是Photo的佔位符,它可能不會(或可能)在那時正確,但稍後可用。 Future上的get方法獲取某個地方正在被佔用的東西,阻止直到它在必要時到達。

此代碼使用FutureTask作爲Future的實現;這需要一個Callable能夠生成一個Photo作爲構造函數參數,並調用它的run方法時調用它。 run的調用通過一個測試來保護,該測試基本重述了if (photo == null)測試的早期版本,但在​​版塊之外(因爲您已經意識到,您確實不想在持有高速緩存鎖的同時加載照片)。

這是我見過或需要幾次的模式。這是一個遺憾,它並不是建立在標準庫的某個地方。

+0

感謝您的想法,非常教育。 – 2013-02-14 09:40:43