Java의 ConcurrentHashMap

xml
Java ConcurrentHashMap 클래스는 Concurrency Collection Classes의 일부입니다.

ConcurrentHashMap

는 해시 테이블 구현으로, 동시 검색 및 업데이트를 지원합니다. 이는 다중 스레드 환경에서 ConcurrentModificationException을 피하기 위해 사용됩니다.

Java ConcurrentHashMap 예제

java.util.concurrent 패키지에서 Java 1.5에서 도입된 동시 클래스는 컬렉션을 반복하는 동안 수정을 시도하면 ConcurrentModificationException이 발생합니다. ConcurrentHashMap은 반복 중에 Map을 수정할 수 있게 해줍니다. ConcurrentHashMap 작업은 스레드로부터 안전합니다. ConcurrentHashMap은 키 및 값에 대해 null을 허용하지 않습니다.

package com.journaldev.util;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {

	public static void main(String[] args) {

		//ConcurrentHashMap
		Map<String,String> myMap = new ConcurrentHashMap<String,String>();
		myMap.put("1", "1");
		myMap.put("2", "1");
		myMap.put("3", "1");
		myMap.put("4", "1");
		myMap.put("5", "1");
		myMap.put("6", "1");
		System.out.println("ConcurrentHashMap before iterator: "+myMap);
		Iterator<String> it = myMap.keySet().iterator();

		while(it.hasNext()){
			String key = it.next();
			if(key.equals("3")) myMap.put(key+"new", "new3");
		}
		System.out.println("ConcurrentHashMap after iterator: "+myMap);

		//HashMap
		myMap = new HashMap<String,String>();
		myMap.put("1", "1");
		myMap.put("2", "1");
		myMap.put("3", "1");
		myMap.put("4", "1");
		myMap.put("5", "1");
		myMap.put("6", "1");
		System.out.println("HashMap before iterator: "+myMap);
		Iterator<String> it1 = myMap.keySet().iterator();

		while(it1.hasNext()){
			String key = it1.next();
			if(key.equals("3")) myMap.put(key+"new", "new3");
		}
		System.out.println("HashMap after iterator: "+myMap);
	}

}

ConcurrentHashMap before iterator: {1=1, 5=1, 6=1, 3=1, 4=1, 2=1}
ConcurrentHashMap after iterator: {1=1, 3new=new3, 5=1, 6=1, 3=1, 4=1, 2=1}
HashMap before iterator: {3=1, 2=1, 1=1, 6=1, 5=1, 4=1}
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793)
	at java.util.HashMap$KeyIterator.next(HashMap.java:828)
	at com.test.ConcurrentHashMapExample.main(ConcurrentHashMapExample.java:44)

ConcurrentHashMap은 맵에서의 새로운 항목을 동시에 처리하는 반면, HashMap은 ConcurrentModificationException을 throw합니다. 예외 스택 트레이스를 자세히 살펴보면 다음 문장이 예외를 throw했음을 알 수 있습니다.

String key = it1.next();

이는 HashMap에 새로운 항목이 삽입되었지만 Iterator가 실패했다는 것을 의미합니다. 실제로 컬렉션 객체의 Iterator는 fail-fast합니다. 즉, 컬렉션 객체의 구조나 항목 수에 대한 어떠한 변경도 예외를 발생시킵니다.

Iterator는 컬렉션의 변경 내용을 어떻게 알 수 있을까요?

우리는 HashMap에서 키의 집합을 가져와서 그 위에서 반복하고 있습니다. HashMap에는 변경 횟수를 세는 변수가 있으며, Iterator는 다음 항목을 가져올 때 next() 함수를 호출할 때 이 변수를 사용합니다. HashMap.java:

/**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient volatile int modCount;

위의 코드를 약간 수정하여 새로운 항목을 삽입할 때 iterator 루프에서 빠져나올 수 있도록 해보겠습니다. put 호출 후에 break 문을 추가하기만 하면 됩니다.

if(key.equals("3")){
	myMap.put(key+"new", "new3");
	break;
}

위의 코드로 출력한 결과는 다음과 같습니다:

ConcurrentHashMap before iterator: {1=1, 5=1, 6=1, 3=1, 4=1, 2=1}
ConcurrentHashMap after iterator: {1=1, 3new=new3, 5=1, 6=1, 3=1, 4=1, 2=1}
HashMap before iterator: {3=1, 2=1, 1=1, 6=1, 5=1, 4=1}
HashMap after iterator: {3=1, 2=1, 1=1, 3new=new3, 6=1, 5=1, 4=1}

키 값이 수정된다면 어떻게 될까요?

그러면 새 항목을 추가하지 않고 기존의 키-값 쌍을 업데이트한다면 어떻게 될까요? 예외가 발생할까요? 원본 프로그램의 코드를 변경하여 확인해 보겠습니다.

//myMap.put(key+"new", "new3");
myMap.put(key, "new3");

컬렉션이 수정되었지만 구조는 동일하므로 예외가 발생하지 않을 것입니다.

더 읽어보기

컬렉션 객체 및 반복자를 생성할 때 각괄호를 주목했나요? 이를 제네릭스라고 하며 런타임에서 ClassCastException을 방지하기 위해 컴파일 시간에 타입 검사를 수행할 때 매우 강력합니다. 자바 제네릭스 예제에서 제네릭스에 대해 더 자세히 알아보세요. 또한 자바 컬렉션 인터뷰 질문자바 이터레이터 디자인 패턴을 읽어보시기를 권합니다.

더 많은 자바 컬렉션 예제는 GitHub 리포지토리에서 확인할 수 있습니다.

참고: API 문서

Source:
https://www.digitalocean.com/community/tutorials/concurrenthashmap-in-java