Post

Java Collections.synchronizedMap과 ConcurrentHashMap 차이점

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()과 ConcurrentHashMap은 null 키와 값을 다르게 처리한다.

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) 성능 비교

ConcurrentHashMap과 Collections.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

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

4) 사용시기

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

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

[출처 및 참고]

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