2013-02-13 1 views
0

커먼즈 컬렉션 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)를 실행하게하는 경합 조건이라고 가정하고있다. 그러나 데이터는 두 스레드에서 동일하므로 문제는되지 않습니다.

내 이유는 정확합니까? 그렇지 않은 경우 큰 성능이 저하되지 않으면 서 동기화 블록을 사용할 수 있습니까? 한 번에 하나의 스레드 만지도에 액세스하면 과도한 느낌이납니다.

답변

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 여기에 관련이없는 사진을로드의 실제 본질적인 측면을하는 방법이다. 사용자의 유효성 검사가이 메서드를 호출하는 다른 메서드에서 수행된다고 가정합니다. 그 외, 이것은 귀하의 코드입니다. 당신이 포장 FutureTask의를 수용 할 수 CacheHandler.photoCache의 유형을 변경해야

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(); 
} 

참고 :

은 우리가 대신 할 것은 이것이다. 이 코드는 명시 적 잠금을 수행하므로 synchronizedMap을 제거 할 수 있습니다. 캐시에 ConcurrentMap을 사용할 수도 있습니다. putIfAbsent을 사용하면 null/put/unlock 시퀀스에 대한 잠금/가져 오기/확인에 더 많은 대안이 될 수 있습니다.

여기서 일어나는 일은 상당히 분명합니다. 캐시에서 무언가를 얻고, 얻은 것이 null인지 확인하기위한 기본 패턴은 null이며, 그렇다면 다시 무언가를 넣는 것이 여전히 있습니다. 그러나 Photo을 입력하는 대신 Future을 넣습니다.이 번호는 Photo의 자리 표시 자이며 그 시점에 없거나 나중에 나타날 수 있습니다. 메서드는 Future에 저장되어있는 장소를 확보하고 필요한 경우 도착할 때까지 차단합니다.

이 코드는 Future의 구현으로 FutureTask을 사용합니다. 이 경우 Callable이 생성자 인수로 Photo을 생성 할 수 있으며 run 메서드가 호출 될 때이를 호출합니다. run에 대한 호출은 본질적으로 if (photo == null) 테스트를 본질적으로 반복하지만, 실제로는 캐시 잠금을 유지하면서 사진을로드하고 싶지 않기 때문에 synchronized 블록을 벗어난 테스트를 통해 테스트됩니다.

이것은 몇 번이나 본 적이 있거나 필요한 패턴입니다. 표준 라이브러리에 어딘가에 내장되어 있지 않은 것은 부끄러운 일입니다.

+0

아이디어를 주셔서 감사합니다. 매우 교육적입니다. –

관련 문제