Nice programing

ConcurrentMap의 putIfAbsent를 사용하기 전에 맵에 키가 포함되어 있는지 확인해야합니다.

nicepro 2020. 11. 4. 08:27
반응형

ConcurrentMap의 putIfAbsent를 사용하기 전에 맵에 키가 포함되어 있는지 확인해야합니다.


여러 스레드에서 사용할 수있는 맵에 Java의 ConcurrentMap을 사용하고 있습니다. putIfAbsent는 훌륭한 방법이며 표준 맵 작업을 사용하는 것보다 읽기 / 쓰기가 훨씬 쉽습니다. 다음과 같은 코드가 있습니다.

ConcurrentMap<String, Set<X>> map = new ConcurrentHashMap<String, Set<X>>();

// ...

map.putIfAbsent(name, new HashSet<X>());
map.get(name).add(Y);

가독성 측면에서는 훌륭하지만 이미 맵에 있더라도 매번 새로운 HashSet을 생성해야합니다. 다음과 같이 작성할 수 있습니다.

if (!map.containsKey(name)) {
    map.putIfAbsent(name, new HashSet<X>());
}
map.get(name).add(Y);

이 변경으로 인해 약간의 가독성이 떨어지지 만 매번 HashSet을 만들 필요가 없습니다. 이 경우 어느 것이 더 낫습니까? 나는 더 읽기 쉽기 때문에 첫 번째 것을 선호하는 경향이 있습니다. 두 번째는 더 나은 성능을 발휘하고 더 정확할 수 있습니다. 아마도 이것보다 더 좋은 방법이있을 것입니다.

이런 방식으로 putIfAbsent를 사용하는 가장 좋은 방법은 무엇입니까?


동시성은 어렵습니다. 간단한 잠금 대신 동시 맵을 사용하려는 경우에는 그렇게하는 것이 좋습니다. 실제로 필요 이상으로 조회를 수행하지 마십시오.

Set<X> set = map.get(name);
if (set == null) {
    final Set<X> value = new HashSet<X>();
    set = map.putIfAbsent(name, value);
    if (set == null) {
        set = value;
    }
}

(일반적인 stackoverflow 면책 조항 : 머리 위를 벗어났습니다. 테스트되지 않았습니다. 컴파일되지 않았습니다. 등)

업데이트 : 1.8은에 computeIfAbsent기본 메서드를 추가 했습니다 ConcurrentMap( Map그 구현이에서 잘못 될 것이기 때문에 흥미 롭습니다 ConcurrentMap). (그리고 1.7은 "다이아몬드 연산자"를 추가했습니다 <>.)

Set<X> set = map.computeIfAbsent(name, n -> new HashSet<>());

(참고,에 HashSet포함 된 의 모든 작업의 ​​스레드 안전성에 대한 책임은 귀하에게 있습니다 ConcurrentMap.)


ConcurrentMap에 대한 API 사용이 진행되는 한 Tom의 대답은 정확합니다. putIfAbsent 사용을 피하는 대안은 제공된 함수로 값을 자동으로 채우고 모든 스레드 안전성을 처리하는 GoogleCollections / Guava MapMaker의 컴퓨팅 맵을 사용하는 것입니다. 실제로 키당 하나의 값만 생성하고 생성 기능이 비싸면 값을 사용할 수있을 때까지 동일한 키를 요청하는 다른 스레드가 차단됩니다.

Guava 11에서 편집 한 MapMaker는 더 이상 사용되지 않으며 Cache / LocalCache / CacheBuilder 항목으로 대체됩니다. 이것은 사용법이 조금 더 복잡하지만 기본적으로 동형입니다.


Eclipse Collections (이전 GS Collections ) MutableMap.getIfAbsentPut(K, Function0<? extends V>)에서 사용할 수 있습니다 .

을 호출 get()하고 null 검사를 수행 한 다음 호출하는 것보다 장점 putIfAbsent()은 키의 hashCode를 한 번만 계산하고 해시 테이블에서 올바른 지점을 한 번만 찾는다는 것입니다. 같은 ConcurrentMaps org.eclipse.collections.impl.map.mutable.ConcurrentHashMap에서의 구현은 getIfAbsentPut()스레드로부터 안전하고 원자 적입니다.

import org.eclipse.collections.impl.map.mutable.ConcurrentHashMap;
...
ConcurrentHashMap<String, MyObject> map = new ConcurrentHashMap<>();
map.getIfAbsentPut("key", () -> someExpensiveComputation());

의 구현 org.eclipse.collections.impl.map.mutable.ConcurrentHashMap은 진정으로 차단되지 않습니다. 불필요하게 팩토리 함수를 호출하지 않도록 모든 노력을 기울이지 만 경합 중에 두 번 이상 호출 될 가능성이 여전히 있습니다.

이 사실은 Java 8의 ConcurrentHashMap.computeIfAbsent(K, Function<? super K,? extends V>). 이 메소드에 대한 Javadoc은 다음과 같이 설명합니다.

전체 메서드 호출은 원자 적으로 수행되므로 함수는 키당 최대 한 번만 적용됩니다. 다른 스레드가이 맵에서 시도한 일부 업데이트 작업은 계산이 진행되는 동안 차단 될 수 있으므로 계산은 짧고 간단해야합니다.

참고 : 저는 Eclipse Collections의 커미터입니다.


각 스레드에 대해 미리 초기화 된 값을 유지하면 허용되는 답변을 개선 할 수 있습니다.

Set<X> initial = new HashSet<X>();
...
Set<X> set = map.putIfAbsent(name, initial);
if (set == null) {
    set = initial;
    initial = new HashSet<X>();
}
set.add(Y);

최근에 Set이 아닌 AtomicInteger 맵 값과 함께 이것을 사용했습니다.


5 년 이상 동안 아무도이 문제를 해결하기 위해 ThreadLocal사용하는 솔루션을 언급하거나 게시하지 않았다는 것을 믿을 수 없습니다 . 이 페이지의 몇 가지 솔루션 은 스레드로부터 안전하지 않으며 엉성합니다.

이 특정 문제에 대해 ThreadLocals를 사용 하는 것은 동시성에 대한 모범 사례간주 될 뿐만 아니라 스레드 경합 가비지 / 객체 생성을 최소화하기위한 것 입니다. 또한 엄청나게 깨끗한 코드입니다.

예를 들면 :

private final ThreadLocal<HashSet<X>> 
  threadCache = new ThreadLocal<HashSet<X>>() {
      @Override
      protected
      HashSet<X> initialValue() {
          return new HashSet<X>();
      }
  };


private final ConcurrentMap<String, Set<X>> 
  map = new ConcurrentHashMap<String, Set<X>>();

그리고 실제 논리 ...

// minimize object creation during thread contention
final Set<X> cached = threadCache.get();

Set<X> data = map.putIfAbsent("foo", cached);
if (data == null) {
    // reset the cached value in the ThreadLocal
    listCache.set(new HashSet<X>());
    data = cached;
}

// make sure that the access to the set is thread safe
synchronized(data) {
    data.add(object);
}

내 일반적인 근사치 :

public class ConcurrentHashMapWithInit<K, V> extends ConcurrentHashMap<K, V> {
  private static final long serialVersionUID = 42L;

  public V initIfAbsent(final K key) {
    V value = get(key);
    if (value == null) {
      value = initialValue();
      final V x = putIfAbsent(key, value);
      value = (x != null) ? x : value;
    }
    return value;
  }

  protected V initialValue() {
    return null;
  }
}

그리고 사용의 예 :

public static void main(final String[] args) throws Throwable {
  ConcurrentHashMapWithInit<String, HashSet<String>> map = 
        new ConcurrentHashMapWithInit<String, HashSet<String>>() {
    private static final long serialVersionUID = 42L;

    @Override
    protected HashSet<String> initialValue() {
      return new HashSet<String>();
    }
  };
  map.initIfAbsent("s1").add("chao");
  map.initIfAbsent("s2").add("bye");
  System.out.println(map.toString());
}

참고 URL : https://stackoverflow.com/questions/3752194/should-you-check-if-the-map-containskey-before-using-concurrentmaps-putifabsent

반응형