멀티스레드 환경에서 HashMap을 썼다가 무한루프에 빠진 이유
문제 발생
여러 스레드가 공유하는 HashMap에 동시에 값을 넣는 코드에서 드물게 CPU 사용률이 100%로 치솟고 응답이 없는 상태가 발생했습니다.
Map<String, Integer> counters = new HashMap<>(); // 스레드 안전하지 않음
// 여러 스레드가 동시에 counters.put(...) 호출원인 분석
HashMap은 스레드 안전을 보장하지 않습니다. 여러 스레드가 동시에 리사이징(내부 배열 크기를 늘리는 과정)을 수행하면, 특정 JDK 버전의 구현에서는 내부 연결 리스트(버킷 체인)가 서로를 순환 참조하는 상태가 될 수 있고, 이후 get()이 그 순환 구조를 무한히 순회하며 CPU를 점유합니다. 데이터가 조용히 유실되거나 덮어써지는 문제도 함께 발생할 수 있습니다.
이 문제는 재현이 까다롭습니다 — 특정 타이밍에 여러 스레드가 정확히 겹쳐야 발생하므로, 테스트 환경에서는 멀쩡하다가 운영 환경의 부하에서만 나타나는 경우가 많습니다.
해결 방안
- 여러 스레드가 동시에 읽고 쓰는 Map이라면
java.util.concurrent.ConcurrentHashMap을 씁니다. 내부적으로 세그먼트/버킷 단위로 락을 분산해Hashtable처럼 전체를 잠그지 않으면서도 스레드 안전을 보장합니다.
Map<String, Integer> counters = new ConcurrentHashMap<>();- 읽기가 압도적으로 많고 쓰기가 드물다면
Collections.synchronizedMap(new HashMap<>())도 대안이지만, 모든 접근이 하나의 락을 공유해ConcurrentHashMap보다 동시성이 떨어집니다. - 카운터처럼 단순 증가/감소가 목적이라면
ConcurrentHashMap.merge()나LongAdder가 더 적합할 수 있습니다 —get()후put()하는 2단계 연산은 그 사이에 다른 스레드가 끼어들 수 있어 원자적이지 않습니다. - 애초에 여러 스레드가 같은 가변 상태를 공유하지 않도록 설계할 수 있는지도 검토합니다 — 스레드마다 로컬 상태를 갖고 마지막에 합치는 방식이 락 경합 자체를 없앨 수 있습니다.
공식 문서
- Oracle Java Docs: ConcurrentHashMap
- Oracle Java Docs: HashMap — "Note that this implementation is not synchronized"
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.