자바 동기화 블록 대 Collections.synchronizedMap


85

다음 코드가 호출을 올바르게 동기화하도록 설정되어 synchronizedMap있습니까?

public class MyClass {
  private static Map<String, List<String>> synchronizedMap = Collections.synchronizedMap(new HashMap<String, List<String>>());

  public void doWork(String key) {
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) {
      //do something with values
    }
  }

  public static void addToMap(String key, String value) {
    synchronized (synchronizedMap) {
      if (synchronizedMap.containsKey(key)) {
        synchronizedMap.get(key).add(value);
      }
      else {
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
      }
    }
  }
}

내 이해에 따르면 addToMap()다른 스레드가 호출하지 못하도록 remove()또는 containsKey()호출을 받기 전에 put()동기화 된 블록이 필요하지만 원래 맵을 만들었 doWork()기 때문에 다른 스레드가 반환 addToMap()전에 동기화 된 블록에 들어갈 수 없기 때문에 동기화 된 블록이 필요하지 않습니다. remove()와 함께 Collections.synchronizedMap(). 그 맞습니까? 이 작업을 수행하는 더 좋은 방법이 있습니까?

답변:


90

Collections.synchronizedMap() 맵에서 실행하려는 각 원자 적 작업이 동기화되도록합니다.

그러나 맵에서 두 개 이상의 작업을 실행하려면 블록에서 동기화해야합니다. 예-올바르게 동기화하고 있습니다.


26
javadocs가 동기화 맵이 내부 잠금이 아닌 맵 자체에서 동기화됨을 명시 적으로 명시하기 때문에 이것이 작동한다는 것을 언급하는 것이 좋을 것이라고 생각합니다. 그 경우 동기화 된 경우 (synchronizedMap) 올바르지 않습니다.
extraneon

2
@Yuval이 답변을 좀 더 깊이 설명해 주시겠습니까? 당신은 sychronizedMap이 작업을 원자 적으로 수행한다고 말했지만 syncMap이 모든 작업을 원자 적으로 만든 경우 왜 자신의 동기화 블록이 필요합니까? 첫 번째 단락은 두 번째 단락에 대한 걱정을 배제하는 것 같습니다.
almel

내 볼 @almel 대답
세르게이

2
지도가 이미 사용하고 있기 때문에 동기화 블록이 필요한 이유는 Collections.synchronizedMap()무엇입니까? 나는 두 번째 요점을 얻지 못하고있다.
Bimal Sharma


13

코드에 미묘한 버그 가있을 가능성 이 있습니다 .

[ 업데이트 : 그가 map.remove ()를 사용하고 있기 때문에이 설명은 완전히 유효하지 않습니다. 나는 그 사실을 처음으로 놓쳤다. :( 나는대로 나머지를 떠나 있지만,이 말을 리드 문을 변경하고 있습니다. 그 지적에 대한 질문의 저자 덕분에 잠재적 버그를.]

doWork () 에서는 스레드로부터 안전한 방식으로 Map에서 List 값을 가져옵니다. 그러나 이후에 안전하지 않은 문제의 목록에 액세스하게됩니다. 예를 들어, 한 스레드는 doWork () 의 목록을 사용하고 다른 스레드는 addToMap () 에서 synchronousMap.get (key) .add (value) 를 호출 할 수 있습니다 . 이 두 액세스는 동기화되지 않습니다. 경험상의 규칙은 컬렉션의 스레드 안전 보장이 저장하는 키 또는 값으로 확장되지 않는다는 것입니다.

다음과 같이 동기화 된 목록을지도에 삽입하여이 문제를 해결할 수 있습니다.

List<String> valuesList = new ArrayList<String>();
valuesList.add(value);
synchronizedMap.put(key, Collections.synchronizedList(valuesList)); // sync'd list

또는 doWork () 의 목록에 액세스하는 동안지도에서 동기화 할 수 있습니다 .

  public void doWork(String key) {
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) {
      synchronized (synchronizedMap) {
          //do something with values
      }
    }
  }

마지막 옵션은 동시성을 약간 제한하지만 다소 명확한 IMO입니다.

또한 ConcurrentHashMap에 대한 빠른 메모입니다. 이것은 정말 유용한 클래스이지만 동기화 된 HashMaps를 항상 적절하게 대체하는 것은 아닙니다. Javadocs에서 인용하면,

이 클래스는 스레드 안전성에 의존 하지만 동기화 세부 사항에 의존 하지 않는 프로그램에서 Hashtable과 완전히 상호 운용됩니다 .

즉, putIfAbsent ()는 원자 삽입에 적합하지만 해당 호출 중에 맵의 다른 부분이 변경되지 않을 것이라고 보장하지는 않습니다. 그것은 원 자성을 보장합니다. 샘플 프로그램에서 put () 이외의 항목에 대해 (동기화 된) HashMap의 동기화 세부 사항에 의존하고 있습니다.

마지막 것. :) Java Concurrency in Practice 의이 훌륭한 인용문은 항상 디버깅 다중 스레드 프로그램을 설계하는 데 도움이됩니다.

둘 이상의 스레드에서 액세스 할 수있는 각 변경 가능한 상태 변수에 대해 해당 변수에 대한 모든 액세스는 동일한 잠금을 보유한 상태에서 수행되어야합니다.


syncedMap.get ()으로 목록에 액세스하면 버그에 대한 귀하의 요점이 보입니다. remove ()를 사용하고 있으므로 해당 키로 다음 추가가 새 ArrayList를 생성하고 doWork에서 사용하는 것을 방해하지 않아야합니까?
Ryan Ahearn

옳은! 나는 당신의 제거를 완전히 지나쳤습니다.
JLR

1
둘 이상의 스레드에서 액세스 할 수있는 각 변경 가능한 상태 변수에 대해 해당 변수에 대한 모든 액세스는 동일한 잠금을 보유한 상태에서 수행되어야합니다. ---- 일반적으로 새 Object () 인 개인 속성을 추가하고이를 동기화 블록에 사용합니다. 그렇게하면 그 맥락에 대한 모든 것을 알 수 있습니다. 동기화 됨 (
objectInVar

11

예, 올바르게 동기화하고 있습니다. 좀 더 자세히 설명하겠습니다. syncedMap 객체에 대한 메서드 호출 순서에서 후속 메서드 호출의 이전 메서드 호출 결과에 의존해야하는 경우에만 두 개 이상의 메서드 호출을 동기화해야합니다. 이 코드를 살펴 보겠습니다.

synchronized (synchronizedMap) {
    if (synchronizedMap.containsKey(key)) {
        synchronizedMap.get(key).add(value);
    }
    else {
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
    }
}

이 코드에서

synchronizedMap.get(key).add(value);

synchronizedMap.put(key, valuesList);

메서드 호출은 이전의 결과에 의존합니다.

synchronizedMap.containsKey(key)

메서드 호출.

메서드 호출 시퀀스가 ​​동기화되지 않은 경우 결과가 잘못되었을 수 있습니다. 예를 들어, thread 1상기 방법을 실행 addToMap()하고 thread 2상기 방법을 실행하는 doWork() 상의 메소드 호출 순서 synchronizedMap로서 다음 객체 결과 : Thread 1상기 방법을 실행 한

synchronizedMap.containsKey(key)

결과는 " true"입니다. 그 운영 시스템에 실행 제어를 전환 한 후, thread 2그것을 실행 한

synchronizedMap.remove(key)

그 후 실행 제어가로 다시 전환 thread 1되고 예를 들어 실행되었습니다.

synchronizedMap.get(key).add(value);

믿는 synchronizedMap목적은을 포함 key하고 NullPointerException있기 때문에 발생합니다 synchronizedMap.get(key) 의지 반환 null. synchronizedMap객체 에 대한 메서드 호출 시퀀스가 서로의 결과에 의존하지 않는 경우 시퀀스를 동기화 할 필요가 없습니다. 예를 들어 다음 시퀀스를 동기화 할 필요가 없습니다.

synchronizedMap.put(key1, valuesList1);
synchronizedMap.put(key2, valuesList2);

여기

synchronizedMap.put(key2, valuesList2);

메서드 호출은 이전 결과에 의존하지 않습니다.

synchronizedMap.put(key1, valuesList1);

메서드 호출 (일부 스레드가 두 메서드 호출 사이에 간섭을 일으키고 예를 들어를 제거했는지 여부는 중요하지 않음 key1).


4

나에게 맞는 것 같습니다. 변경 사항이 있으면 Collections.synchronizedMap () 사용을 중지하고 모든 것을 동일한 방식으로 동기화하여 더 명확하게합니다.

또한

  if (synchronizedMap.containsKey(key)) {
    synchronizedMap.get(key).add(value);
  }
  else {
    List<String> valuesList = new ArrayList<String>();
    valuesList.add(value);
    synchronizedMap.put(key, valuesList);
  }

List<String> valuesList = synchronziedMap.get(key);
if (valuesList == null)
{
  valuesList = new ArrayList<String>();
  synchronziedMap.put(key, valuesList);
}
valuesList.add(value);

3
할 일. 우리가 사용해야하는 이유 나는 그것을 얻지 않는다 Collections.synchronizedXXX()우리는 여전히이있을 때 우리의 일상 응용 프로그램의 로직 (대부분의 경우 단지 콜렉션 자체가 될 것이다)하는 오브젝트 동기화에 API를
kellogs

3

동기화 한 방식이 정확합니다. 하지만 캐치가 있습니다

  1. 컬렉션 프레임 워크에서 제공하는 동기화 된 래퍼는 메서드 호출 즉, add / get / contains가 상호 배타적으로 실행되도록합니다.

그러나 실제 세계에서는 일반적으로 값을 입력하기 전에 맵을 쿼리합니다. 따라서 두 가지 작업을 수행해야하므로 동기화 된 블록이 필요합니다. 그래서 당신이 그것을 사용한 방식이 정확합니다. 하나.

  1. Collection 프레임 워크에서 사용할 수있는 Map의 동시 구현을 사용할 수 있습니다. 'ConcurrentHashMap'혜택은

ㅏ. 동일한 작업을 수행하지만보다 효율적인 방식으로 수행하는 API 'putIfAbsent'가 있습니다.

비. 효율성 : dThe CocurrentMap은 키를 잠그기 때문에 전체 맵의 세계를 차단하지 않습니다. 키와 값을 차단 한 위치.

씨. 코드베이스의 다른 곳에서지도 객체의 참조를 전달했을 수 있습니다. 여기서 사용자 / 다른 개발자가 잘못 사용하게 될 수 있습니다. 즉 그는지도의 객체를 잠그지 않고 모두 add () 또는 get () 할 수 있습니다. 따라서 그의 통화는 동기화 블록과 상호 배타적으로 실행되지 않습니다. 그러나 동시 구현을 사용하면 잘못 사용 / 구현 될 수 없다는 안심할 수 있습니다.


2

체크 아웃 구글 컬렉션 ' Multimap의 예를 들어 28 페이지의 프리젠 테이션 .

어떤 이유로 해당 라이브러리를 사용할 수없는 경우 ConcurrentHashMap대신 사용하는 것이 좋습니다 SynchronizedHashMap. putIfAbsent(K,V)요소 목록이 아직없는 경우 원자 적으로 추가 할 수 있는 멋진 방법이 있습니다. 또한 CopyOnWriteArrayList사용 패턴이 그렇게해야하는 경우 맵 값에 사용 을 고려 하십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.