LinkedHashMap은 왜 멀티스레드 환경에서 위험한가?

2025. 10. 26. 16:59·Trouble Shooting/JAVA

멀티스레드 환경에서 index++ 는 왜 위험한가? 에서 index++ 연산의 원자성 문제를 해결하기 위해 AtomicLong을 적용하는 방법을 살펴보았다.
하지만 ID 생성만 안전하다고 해서, 애플리케이션 전체의 동시성이 보장되는 것은 아니다. 게시글이나 사용자 정보를 관리하는 Map 자체가 스레드에 안전하지 않다면, 여전히 데이터 꼬임이나 손실이 발생할 수 있다.

이번 글에서는 LinkedHashMap을 사용할 때 발생할 수 있는 동시성 문제와 이를 해결하기 위한 ConcurrentHashMap의 내부 구조를 비교해본다.

public class InMemoryPostRepository implements PostRepository {
    private AtomicLong postId = new AtomicLong(0);
    //Map<Key : index, Value : PostEntity>
    private Map<Long, PostEntity> Posts = new LinkedHashMap<>();
    
    ...

}

 


LinkedHashMap의 구조

💡 LinkedHashMap 이란?
LinkedHashMap은 HashMap + 이중 연결 리스트(Doubly Linked List) 로 구성되어 있다.

즉, 각 Entry(Node)는 next와 prev 포인터를 가지고 삽입 순서를 유지하도록 만들어졌다.

 

특징 

  • HashMap : 해시 버킷 구조 (해시 인덱스로 계산)
  • Linked : Entry<K,V> 내부에서 next(after)와 prev(before) 포인터로 삽입 순서 유지

단점 

  • 내부적으로 Lock이나 CAS 같은 동기화 메커니즘을 사용하지 않기 때문에, 멀티스레드 환경에서는 안전하지 않다.

 

 

노드 삽입 순서

table[3] → Node(A,1) → Node(B,2) → Node(C,3) → null

 

 

새로운 Node(D,4) 삽입 시

table[3] → Node(A,1) → Node(B,2) → Node(C,3) → Node(D,4)

 

tail에 연결되며 순서를 보장하며, 성능측면에서 빠르다.

 

 

 

 

LinkedHashMap은 단일 스레드 환경에서는 문제가 되지 않지만, 멀티스레드 환경에서 동시에 삽입이 진행되면 내부 포인터 (next, prev)가 꼬이면서 구조 불일치가 발생할 수 있다.

public Post postCreate(User user, PostCreateFormDto postCreateFormDto) {
    long id = postId.getAndIncrement();
    
    Post post = new Post(
        id,
        postCreateFormDto.getTitle(),
        postCreateFormDto.getContent(),
        user.getUserId(),
        LocalDateTime.now()
    );
	
    //LinkedHashMap : 동시에 삽입 시 연결 꼬임 발생 가능
     Posts.put(post.getPostId(), post);

    return post;
}

 

Posts.put이 동시에 실행되면, 하나의 스레드가 연결중인 노드를 다른 스레드가 덮어쓰면서 참조 관계가 꼬일 수 있다.

해당 문제로 인해 데이터가 유실되거나, ConcurrentModificationException 이 발생할 수 있다.

 

💡 ConcurrentModificationException 이란?
한 스레드가 컬렉션을 순회하고 있는 동안
다른 스레드나 자신이 컬렉션의 구조 변경이 발생됨을 확인하면 던지는 예외

 


ConcurrentHashMap의 구조

💡 ConcurrentHashMap 이란?
CAS(CompareAndSwap)과 분할 잠금(Lock Stripping)을 이용해서 멀티스레드 환경에서 안전하다.(Safety)

특징

  • put()시에는 특정 버킷에만 CAS 연산을 적용한다.
  • 전체 Map에 lock을 걸지 않기 때문에 병렬 삽입 가능
  • 기존 노드의 next(after)에 객체를 연결한다.

단점

  • 순서를 보장하지 않는다.

 

 

노드 삽입 순서

table[3] → Node(A,1) → Node(B,2)

 

새로운 노드 Node(C,3) 삽입시

table[3] → Node(C,3) → Node(A,1) → Node(B,2)

 

head에 연결되어 순서를 보장하지 않지만, CAS 기반으로 안전하다. 

 

 

ConcurrentHashMap 내부 구조 살펴보기

    public V put(K key, V value) {
        return putVal(key, value, false);
    }

 

put의 핵심 로직인 putVal()으로 구현되어있다.

final V putVal(K key, V value, boolean onlyIfAbsent) {
    ...
    for (Node<K,V>[] tab = table;;) { // 무한 루프 (CAS 실패 시 재시도)
        Node<K,V> f; int n, i, fh;
        
        // 1. table 초기화 (lazy-init)
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();

        // 2. 버킷이 비어있다면 CAS로 바로 삽입
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;  // Lock 없이 CAS 성공 → 삽입 완료
        }

        // 3. 해시 충돌 발생 or 버킷에 노드가 존재하는 경우
        else {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    // 동일 버킷 내 연결리스트 or 트리 처리
                    ...
                }
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

 

2번에서 버킷이 비어있을 경우, CAS로 lock없이 노드를 추가한다는 사실을 확인할 수 있으며, 3번 해시 충돌 발생 또는 버킷에 노드가 존재할 경우, 해당 버킷을 synchronized 적용하여 부분적으로 잠금(Fine-Grained Locking)을 건다.

 

ConcurrentHashMap로 문제 개선하기

AtomicLong을 통해 ID 생산 과정에 있어 원자성 문제를 해결했더라도, 멀티 스레드 환경에서 같은 버킷에 노드가 연결될 때 안전하게 들어오지 못한다면, Map 내부에서 next,prev 연결 꼬임을 발생시킬 수 있는 LinkedHashMap 때문에 해당 버킷에서 안전하게 노드가 연결될 수 있도록, CAS 기반으로 안전하게 노드를 연결할 수 있는 ConcurrentHashMap을 사용한다. 

private AtomicLong postId = new AtomicLong(0);
//Linked -> Concurrent 변경
private Map<Long, PostEntity> Posts = new ConcurrentHashMap<>();
   
public Post postCreate(User user, PostCreateFormDto postCreateFormDto) {
   	
    long id = postId.getAndIncrement();
    
    Post post = new Post(
        id,
        postCreateFormDto.getTitle(),
        postCreateFormDto.getContent(),
        user.getUserId(),
        LocalDateTime.now()
    );

    Posts.put(post.getPostId(), post);

    return post;
}

 

결국 멀티스레드 환경에서의 동시성 문제는 데이터를 누가 언제  수정하느냐의 싸움이다.

AtomicLong은 값의 원자성, ConcurrentHashMap은 구조의 일관성을 보장한다.
두 가지를 함께 사용해야 스레드 안전성이 완전하게 확보된다. 

'Trouble Shooting > JAVA' 카테고리의 다른 글

멀티스레드 환경에서 index++ 는 왜 위험한가?  (0) 2025.10.26
'Trouble Shooting/JAVA' 카테고리의 다른 글
  • 멀티스레드 환경에서 index++ 는 왜 위험한가?
happy7yong
happy7yong
뭇별에 닿기를
  • happy7yong
    yongyong
    happy7yong
  • 전체
    오늘
    어제
    • 분류 전체보기 (8)
      • JavaScript (4)
      • Coding Test (0)
        • JavaScript (0)
      • Trouble Shooting (2)
        • JAVA (2)
      • 회고 (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
happy7yong
LinkedHashMap은 왜 멀티스레드 환경에서 위험한가?
상단으로

티스토리툴바