Post

Java Collections.synchronizedMap과 ConcurrentHashMap 차이점

1. 차이점

Collections.synchronizedMap()ConcurrentHashMap은 모두 데이터 컬렉션에 대한 스레드 안전 작업을 제공한다.

Collections 유틸리티 클래스는 컬렉션에서 작동하고 래핑된 컬렉션을 반환하는 다형성 알고리즘을 제공한다. 해당 synchronizedMap() 메서드는 스레드로부터 안전한 기능을 제공한다.

이름에서 알 수 있듯이 synchronizedMap()은 매개변수에서 제공하는 Map이 지원 하는 동기화된 Map을 반환한다. 스레드 안전성을 제공하기 위해 synchronizedMap()은 반환된 Map을 통해 백업 Map에 대한 모든 액세스를 허용한다.

ConcurrentHashMap검색 및 업데이트에 대한 높은 동시성을 지원하는 HashMap의 향상 기능으로 JDK 1.5에 도입되었다. HashMap은 스레드로부터 안전하지 않으므로 스레드 경합 중에 잘못된 결과가 발생할 수 있다.

ConcurrentHashMap 클래스는 스레드로부터 안전하다. 따라서 여러 스레드가 복잡한 문제 없이 단일 개체에서 작동할 수 있다.

ConcurrentHashMap에서 읽기 작업은 차단되지 않는 반면 쓰기 작업은 특정 세그먼트 또는 버킷을 잠근다. 기본 버킷 또는 동시성 수준은 16이며, 이는 16개의 스레드가 세그먼트 또는 버킷을 잠근 후 언제든지 쓸 수 있음을 의미한다.

1) ConcurrentModificationException

HashMap과 같은 객체의 경우 동시 작업 수행이 허용되지 않는다. 따라서 반복하는 동안 HashMap을 업데이트하려고 하면 ConcurrentModificationException을 수신하게 된다. 이것은 synchronizedMap()을 사용할 때도 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
@Test(expected = ConcurrentModificationException.class)
public void whenRemoveAndAddOnHashMap_thenConcurrentModificationError() {
    Map<Integer, String> map = new HashMap<>();
    map.put(1, "baeldung");
    map.put(2, "HashMap");
    Map<Integer, String> synchronizedMap = Collections.synchronizedMap(map);
    Iterator<Entry<Integer, String>> iterator = synchronizedMap.entrySet().iterator();
    while (iterator.hasNext()) {
        synchronizedMap.put(3, "Modification");
        iterator.next();
    }
}

그러나 이것은 ConcurrentHashMap의 경우가 아니다.

1
2
3
4
5
6
7
8
9
10
11
Map<Integer, String> map = new ConcurrentHashMap<>();
map.put(1, "baeldung");
map.put(2, "HashMap");
 
Iterator<Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    map.put(3, "Modification");
    iterator.next()
}
 
Assert.assertEquals(3, map.size());

2) null 지원

Collections.synchronizedMap()ConcurrentHashMapnull 키와 값을 다르게 처리한다.

ConcurrentHashMap은 키 또는 값에서 null을 허용하지 않는다.

1
2
3
4
5
@Test(expected = NullPointerException.class)
public void allowNullKey_In_ConcurrentHasMap() {
    Map<String, Integer> map = new ConcurrentHashMap<>();
    map.put(null, 1);
}

그러나 Collections.synchronizedMap()을 사용할 때 null 지원은 입력 Map에 따라 다르다. Collections.synchronizedMap()이 HashMap 또는 LinkedHashMap에 의해 지원되는 경우 키로 하나의 null을 가질 수 있고 null 값을 얼마든지 가질 수 있다. 반면에 TreeMap을 사용하는 경우에는 null 값을 가질 수 있지만 null 키는 가질 수 없다.

HashMap이 지원하는 Collections.synchronizedMap()에 대해 null 키를 사용할 수 있다고 가정해 본다.

1
2
3
4
Map<String, Integer> map = Collections
  .synchronizedMap(new HashMap<String, Integer>());
map.put(null, 1);
Assert.assertTrue(map.get(null).equals(1));

마찬가지로 Collections.synchronizedMap()ConcurrentHashMap 값에서 null 지원을 확인할 수 있다.

3) 성능 비교

ConcurrentHashMapCollections.synchronizedMap()의 성능을 비교해 본다. 이 경우에는 오픈 소스 프레임워크 JMH(Java Microbenchmark Harness)를 사용하여 방법의 성능을 나노초 단위로 비교한다.

이 맵에서 임의 읽기 및 쓰기 작업에 대한 비교를 실행했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Benchmark
public void randomReadAndWriteSynchronizedMap() {
    Map<String, Integer> map = Collections.synchronizedMap(new HashMap<String, Integer>());
    performReadAndWriteTest(map);
}

@Benchmark
public void randomReadAndWriteConcurrentHashMap() {
    Map<String, Integer> map = new ConcurrentHashMap<>();
    performReadAndWriteTest(map);
}

private void performReadAndWriteTest(final Map<String, Integer> map) {
    for (int i = 0; i < TEST_NO_ITEMS; i++) {
        Integer randNumber = (int) Math.ceil(Math.random() * TEST_NO_ITEMS);
        map.get(String.valueOf(randNumber));
        map.put(String.valueOf(randNumber), randNumber);
    }
}

1,000개 항목에 대해 10개의 스레드로 5번의 반복을 사용하여 성능 벤치마크를 실행했다. 벤치마크 결과이다.

1
2
3
4
5
6
7
Benchmark                                                     Mode  Cnt        Score        Error  Units
MapPerformanceComparison.randomReadAndWriteConcurrentHashMap  avgt  100  3061555.822 ±  84058.268  ns/op
MapPerformanceComparison.randomReadAndWriteSynchronizedMap    avgt  100  3234465.857 ±  60884.889  ns/op
MapPerformanceComparison.randomReadConcurrentHashMap          avgt  100  2728614.243 ± 148477.676  ns/op
MapPerformanceComparison.randomReadSynchronizedMap            avgt  100  3471147.160 ± 174361.431  ns/op
MapPerformanceComparison.randomWriteConcurrentHashMap         avgt  100  3081447.009 ±  69533.465  ns/op
MapPerformanceComparison.randomWriteSynchronizedMap           avgt  100  3385768.422 ± 141412.744  ns/op

위의 결과는 ConcurrentHashMapCollections.synchronizedMap()보다 성능이 우수함을 보여준다.

4) 사용시기

데이터 일관성이 가장 중요한 경우 Collections.synchronizedMap()을 선호해야 하며 읽기 작업보다 쓰기 작업이 훨씬 더 많은 성능이 중요한 애플리케이션의 경우 ConcurrentHashMap을 선택해야 한다.

이는 Collections.synchronizedMap()이 각 스레드가 읽기/쓰기 작업 모두에 대해 전체 개체에 대한 잠금을 획득해야 하기 때문이다. 이에 비해 ConcurrentHashMap을 사용하면 스레드가 컬렉션의 개별 세그먼트에 대한 잠금을 획득하고 동시에 수정할 수 있다.

[출처 및 참고]

This post is licensed under CC BY 4.0 by the author.