java - 반복 - 자바에서 한 객체의 스레드 안전 캐시



java thread 반복 (8)

당신이 그것에 대해 어떻게 생각하십니까? 그것에 대해 나쁜 점이 보이십니까?

Bleah - 당신은 하나의 기능으로 인해 여러 기능 (지도 액세스, 동시성 친화적 인 액세스, 지연된 값 생성 등)을 갖춘 복잡한 데이터 구조 인 MapMaker를 사용하고 있습니다 (단일 건설 비싼 객체의 지연 생성) .

코드 재사용이 좋은 목표이지만이 접근법은 추가적인 오버 헤드와 복잡성을 추가합니다. 또한지도 데이터 구조를 볼 때 미래의 유지 관리자를 오도합니다. 키 / 값지도에는 실제로는 단 한 가지 (국가 목록)가있을 때가 있습니다. 단순성, 가독성 및 명확성은 향후 유지 관리의 핵심입니다.

그것을 할 다른 방법이 있습니까? 어떻게하면 좋을까요? 이 경우 완전히 다른 솔루션을 찾아야합니까?

당신이 게으른 로딩을 한 것처럼 보입니다. 다른 게으른 로딩 질문에 대한 해결책을보십시오. 예를 들어,이 중 하나는 고전적인 이중 점검 접근법을 다루고 있습니다 (Java 1.5 또는 이후 버전을 사용하고 있는지 확인하십시오).

Java에서 "이중 확인 잠금이 잘못되었습니다."선언을 해결하는 방법은 무엇입니까?

단순히 여기에서 솔루션 코드를 반복하기보다는 지식 기반을 키우기 위해 이중 체크를 통한 지연로드에 대한 토론을 읽는 것이 유용하다고 생각합니다. (그것이 유쾌한 것처럼 나간다면 미안하다 - 그냥 먹기보다는 물고기를 가르치려고 노력한다.)

애플리케이션 목록에 국가 목록을 반환해야하는 CountryList 객체가 있다고 가정 해 보겠습니다. 국가 로딩은 많은 작업이므로 목록을 캐싱해야합니다.

추가 요구 사항:

  • CountryList는 스레드로부터 안전해야합니다.
  • CountryList는 게으른로드해야합니다 (요청시에만).
  • CountryList는 캐시 무효화를 지원해야합니다.
  • CountryList는 캐시가 매우 드물게 무효화 될 것이라는 점을 고려하여 최적화해야합니다.

나는 다음과 같은 해결책을 찾았다.

public class CountryList {
    private static final Object ONE = new Integer(1);

    // MapMaker is from Google Collections Library    
    private Map<Object, List<String>> cache = new MapMaker()
        .initialCapacity(1)
        .makeComputingMap(
            new Function<Object, List<String>>() {
                @Override
                public List<String> apply(Object from) {
                    return loadCountryList();
                }
            });

    private List<String> loadCountryList() {
        // HEAVY OPERATION TO LOAD DATA
    }

    public List<String> list() {
        return cache.get(ONE);
    }

    public void invalidateCache() {
        cache.remove(ONE);
    }
}

당신이 그것에 대해 어떻게 생각하십니까? 그것에 대해 나쁜 점이 보이십니까? 그것을 할 다른 방법이 있습니까? 어떻게하면 좋을까요? 이 경우 완전히 다른 솔루션을 찾아야합니까?

감사.


(MapMaker가 Google 콜렉션에서 온다고 가정합니다.) 키가 실제로 없기 때문에지도를 사용할 필요가 없지만 모든 발신자에게 구현이 숨겨져 있기 때문에지도로 표시되지 않습니다. 큰 거래.


거기에 라이브러리가 있습니다 ( atlassian에서 ) - LazyReference 라는 util 클래스 중 하나입니다. LazyReference는 지연 생성 될 수있는 객체에 대한 참조입니다 (첫 번째 get시). guarenteed thread safe이며, init는 또한 한번만 발생한다. 두 개의 쓰레드가 동시에 get ()을 호출하면, 한 쓰레드가 계산을하고, 다른 쓰레드는 wait를 블럭한다.

샘플 코드보기 :

final LazyReference<MyObject> ref = new LazyReference() {
    protected MyObject create() throws Exception {
        // Do some useful object construction here
        return new MyObject();
    }
};

//thread1
MyObject myObject = ref.get();
//thread2
MyObject myObject = ref.get();

당신의 요구는 여기에서 꽤 단순 해 보입니다. MapMaker를 사용하면 구현이 더 복잡해집니다. 전체 이중 확인 잠금 관용구는 올바르게 작동하기가 까다 롭고 1.5 이상에서만 작동합니다. 솔직히 말해서, 프로그래밍의 가장 중요한 규칙 중 하나를 위반하고 있습니다.

조숙 한 최적화는 모든 악의 근원입니다.

double-checked locking 이디엄은 캐시가 이미로드 된 경우 동기화 비용을 피하려고 시도합니다. 그러나 그 오버 헤드로 인해 실제로 문제가 발생합니까? 더 복잡한 코드의 비용이 들까 요? 프로파일 링이 당신에게 다르게 지시 할 때까지는 그렇지 않다고 가정합니다.

다음은 제 3 자 코드가 필요없는 매우 간단한 솔루션입니다 (JCIP 주석을 무시함). 빈 목록은 캐시가 아직로드되지 않았다는 것을 전제로합니다. 또한 국가 목록의 내용이 반환 된 목록을 수정할 가능성이있는 클라이언트 코드로 벗어나는 것을 방지합니다. 이 점이 걱정스럽지 않으면 Collections.unmodifiedList ()에 대한 호출을 제거 할 수 있습니다.

public class CountryList {

    @GuardedBy("cache")
    private final List<String> cache = new ArrayList<String>();

    private List<String> loadCountryList() {
        // HEAVY OPERATION TO LOAD DATA
    }

    public List<String> list() {
        synchronized (cache) {
            if( cache.isEmpty() ) {
                cache.addAll(loadCountryList());
            }
            return Collections.unmodifiableList(cache);
        }
    }

    public void invalidateCache() {
        synchronized (cache) {
            cache.clear();
        }
    }

}

이것은 ComputingMap을 사용하는 간단한 방법입니다. 모든 메소드가 동기화되는 죽은 간단한 구현 만 필요하므로 잘되어야합니다. 처음 스레드가 캐시를로드하는 동안 (다른 스레드가 캐시를로드하는 동안) 다른 스레드가 충돌하는 것을 분명히 차단합니다 (그리고 누군가가 invalidateCache 객체를 호출하면 다시 동일하게됩니다 - 여기서 invalidateCache가 캐시를로드해야하는지 여부를 결정해야합니다). 다시 캐스팅하거나, 그냥 null로 처리하고, 블록을 다시 가져 오는 첫 번째 시도를 보냅니다.)하지만 모든 스레드는 잘 통과해야합니다.


지도가 무엇인지 모르겠습니다. 게으른 캐시 된 객체가 필요할 때 일반적으로 다음과 같이 수행합니다.

public class CountryList
{
  private static List<Country> countryList;

  public static synchronized List<Country> get()
  {
    if (countryList==null)
      countryList=load();
    return countryList;
  }
  private static List<Country> load()
  {
    ... whatever ...
  }
  public static synchronized void forget()
  {
    countryList=null;
  }
}

나는 이것이 당신이하고있는 것과 비슷하지만 조금 더 간단하다고 생각합니다. 지도가 필요하면 질문을 단순화했습니다. 괜찮습니다.

스레드로부터 안전하게하려면 get과 forget을 동기화해야합니다.


온 디맨드 홀더 관용구 사용

public class CountryList {
  private CountryList() {}

  private static class CountryListHolder {
    static final List<Country> INSTANCE = new List<Country>();
  }

  public static List<Country> getInstance() {
    return CountryListHolder.INSTANCE;
  }

  ...
}

모든 사람들 , 특히 아이디어를 준 사용자 " gid "에게 감사드립니다 .

필자의 목표는 invalidate () 작업이 매우 드문 것으로 간주되어 get () 작업의 성능을 최적화하는 것이 었습니다.

나는 16 개의 스레드를 시작하는 테스트 클래스를 작성했다. 각 스레드는 get () - Operation을 100 만 번 호출했다. 이 클래스를 사용하여 2 코어 maschine에 대한 구현을 프로파일 링했습니다.

테스트 결과

Implementation              Time
no synchronisation          0,6 sec
normal synchronisation      7,5 sec
with MapMaker               26,3 sec
with Suppliers.memoize      8,2 sec
with optimized memoize      1,5 sec

1) "동기화 없음"은 스레드로부터 안전하지는 않지만 우리가 비교할 수있는 최상의 성능을 제공합니다.

@Override
public List<String> list() {
    if (cache == null) {
        cache = loadCountryList();
    }
    return cache;
}

@Override
public void invalidateCache() {
    cache = null;
}

2) "정상적인 동기화"- 꽤 좋은 performace, 표준 아니 생각할 구현

@Override
public synchronized List<String> list() {
    if (cache == null) {
        cache = loadCountryList();
    }
    return cache;
}

@Override
public synchronized void invalidateCache() {
    cache = null;
}

3) "MapMaker와 함께"- 성능이 매우 좋지 않습니다.

코드 상단의 내 질문을 참조하십시오.

4) "Suppliers.memoize"- 좋은 성능. 그러나 성능이 "정상적인 동기화"와 마찬가지로 우리는 최적화하거나 "정상적인 동기화"를 사용해야합니다.

코드 "gid"사용자의 대답을 참조하십시오.

5) "최적화 된 메모와 함께" - 성능은 "동기화되지 않음"구현과 유사하지만 스레드로부터 안전한 성능입니다. 이것이 우리가 필요로하는 것입니다.

캐시 클래스 자체 : (여기 사용 된 공급자 인터페이스는 Google Collections Library에서 가져온 것으로 get () 메소드가 하나뿐입니다. Supplier 참조하십시오. Supplier )

public class LazyCache<T> implements Supplier<T> {
    private final Supplier<T> supplier;

    private volatile Supplier<T> cache;

    public LazyCache(Supplier<T> supplier) {
        this.supplier = supplier;
        reset();
    }

    private void reset() {
        cache = new MemoizingSupplier<T>(supplier);
    }

    @Override
    public T get() {
        return cache.get();
    }

    public void invalidate() {
        reset();
    }

    private static class MemoizingSupplier<T> implements Supplier<T> {
        final Supplier<T> delegate;
        volatile T value;

        MemoizingSupplier(Supplier<T> delegate) {
            this.delegate = delegate;
        }

        @Override
        public T get() {
            if (value == null) {
                synchronized (this) {
                    if (value == null) {
                        value = delegate.get();
                    }
                }
            }
            return value;
        }
    }
}

사용 예 :

public class BetterMemoizeCountryList implements ICountryList {

    LazyCache<List<String>> cache = new LazyCache<List<String>>(new Supplier<List<String>>(){
        @Override
        public List<String> get() {
            return loadCountryList();
        }
    });

    @Override
    public List<String> list(){
        return cache.get();
    }

    @Override
    public void invalidateCache(){
        cache.invalidate();
    }

    private List<String> loadCountryList() {
        // this should normally load a full list from the database,
        // but just for this instance we mock it with:
        return Arrays.asList("Germany", "Russia", "China");
    }
}




lazy-loading