새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.

3 분 소요

목표 D-day : 95 일
자바에서 제공하는 자료 구조중 HashTable과 ConcurrentHashMap의 동작 차이를 학습하려고 합니다.

학습 목표

  • CAS와 Synchronized의 동작 차이
  • 실제 테스트 코드로 속도 차이 확인하기
  • 트레이드 오프 비교해보기

CAS와 Synchronized란

자바는 멀티 쓰레드 환경에서 동적으로 생성된 객체를 관리하는 힙 메모리 영역을 포함한 모든 메모리 영역을 공유하게 됩니다. 여러 쓰레드가 동시에 힙 메모리에 저장된 자료구조에 접근하여 데이터를 변경하려고 할 때, race condition이 발생할 수 있습니다. 사용자가 원치 않는 결과나 상태 변경이 발생할 수 있습니다.

동시성 제어를 위한 여러가지 방법이 있습니다.

  1. 불변 객체 사용으로 thread-safe하게 사용하기
  2. CAS방식인 Atomic클래스 사용하기
  3. Synchronized 키워드 사용하기
  4. 그 외 ..

오늘은 CAS와 Synchronized를 비교하면서 어떤 트레이드 오프가 있는지 확인하려고 합니다.

Synchronized 방식

키워드
  • 잠금
  • 대기

이 방식은 객체가 가지고 있는 모니터 락(moniter lock)을 사용하여 동시성을 제어합니다.

객체를 1인용 화장실이라고 비유하겠습니다. 열쇠는 하나만 존재합니다. 이때 이 열쇠를 모니터 락이라고 합니다.

여러 사람들(쓰레드)가 동시에 접근해도 이 열쇠를 가진 한 사람만 화장실에 들어갈 수 있도록 제어하는 방식입니다.

자바 코드를 보면 다음과 같습니다.

class Restroom {
    private final Object key = this; // 화장실 키 (모니터락 역할)

    public void useRestroom(String person) {
        // 임계구간 critical section
        synchronized (key) { // 화장실 키를 사용하여 동기화
            //.. 내부 로직
        }
    }
}

synchronized(key)의 의미는 쓰레드가 임계구간(critical section)에 접근하게 된다면 RestRoom 인스턴스의 key 제어권을 가지고 있는지 확인합니다.

  • 제어권이 없는 경우 : 무한 대기
  • 제어권이 있는 경우 : 입장

이런 경우라면 100명이건 만명이건 한명이 용건을 보고 나가고 시간이 지나서 다른 사람이 오는 방식으로 진행된다면 성능 저하는 거의 없다고 생각합니다.

만약 10명이 동시에 화장실에 접근한다면 1명은 용건을 처리할 수 있고 나머지 9명은 대기를 하게 됩니다. 이렇게 진행된다면 성능 저하가 발생하게 됩니다.

CAS(Compare-And-Set) 방식

키워드
  • 원자 데이터
  • 반복

CAS 방식은 synchronized를 사용한 동시성 제어로 인한 성능 저하를 최소화하기 위한 방법입니다.

synchronized 방식은 잠금을 얻고 반납하는 과정이 필연적으로 발생하지만, CAS 방식은 잠금을 사용하지 않습니다.

CAS 방식은 다음과 같은 과정을 따릅니다:

  1. 수정하려는 데이터를 검색 후 변수 A에 저장합니다.
  2. 변수 A의 예상된 새 값을 계산합니다.
  3. 같은 조건으로 데이터를 다시 검색 후 변수 B에 저장합니다.
  4. 변수 A와 변수 B를 비교합니다.
    • 값이 같을 경우, 변수 A를 예상된 새 값으로 원자적으로 변경합니다.
    • 값이 다를 경우, 1번부터 다시 시도합니다.

CAS는 CPU 캐시를 읽지 않고 RAM의 데이터를 조회하게 됩니다.

CPU는 연산을 빠르게 하기 위해 캐시에 데이터를 저장하고 연산 후 메모리에 반영하는 과정이 발생할 수 있습니다. CAS (Compare-And-Swap) 방식은 최신 객체의 상태를 확인하고 비교하는 과정을 거쳐 값을 교체하는 방식입니다. 이 과정에서 CPU 캐시를 읽지 않고 RAM의 데이터를 조회하게 됩니다. 이렇게 원자적인 연산을 수행하여 동시성 제어를 가능하게 합니다.

트레이드 오프: CAS 방식 vs Synchronized 방식

비교 항목 CAS 방식 Synchronized 방식
컨텍스트 스위칭 컨텍스트 스위칭이 발생해도 재시도 과정에서 오버헤드가 적음 잠금 획득 및 해제 과정에서 오버헤드가 발생
경량화된 연산 잠금을 사용하지 않아 잠금 관리로 인한 오버헤드가 없음 잠금을 사용하여 안정적으로 동기화
반복적인 재시도 충돌 발생 시 단순히 다시 시도, 대기 상태에 빠지지 않음 잠금을 획득한 쓰레드가 실행 중에 컨텍스트 스위칭이 발생하면 다른 쓰레드는 대기
메모리 접근 최신 데이터를 메모리에서 읽어야 하므로 CPU 캐시 혜택을 덜 받을 수 있음 잠금을 통해 동기화된 접근이 이루어짐
동시성 문제가 많은 환경 잠금으로 인한 대기 시간을 피하면서 효율적으로 해결 여러 쓰레드가 잠금을 획득하려고 경쟁, 성능 저하 가능
읽기 작업이 많은 환경 쓰기 작업에서만 충돌이 발생하므로 효율적 모든 작업에서 잠금 획득 필요
쓰기 작업이 많은 환경 충돌 빈도가 높아지면 반복적인 재시도로 CPU 사용률 증가 충돌을 피하고 안정적으로 동기화 보장
성능 저하 가능성 경합이 심한 경우 성능 저하, 특히 값이 자주 변경되면 반복적인 재시도로 인해 바쁜 대기 상태 가능 잠금 관리로 인한 오버헤드 발생, 대기 시간 증가
  • 컨텍스트 스위칭이 빈번한 환경: CAS 방식이 성능 면에서 더 유리할 수 있습니다. CAS 방식은 잠금 관리로 인한 추가적인 오버헤드가 없으며, 쓰레드가 대기 상태에 빠지지 않습니다.
  • 동시성 문제가 적은 환경: Synchronized 방식이 더 나은 선택일 수 있습니다. 속도 차이는 크지 않지만 Synchronized 방식은 코드가 간단하고 이해하기 쉽습니다.
  • 동시성 문제가 많은 환경: CAS 방식이 더 나은 선택일 수 있습니다. CAS 방식은 잠금으로 인한 대기 시간을 피하면서 동시성 문제를 효율적으로 해결할 수 있습니다.
  • 읽기 작업이 많은 환경: CAS 방식이 더 효율적일 수 있습니다. CAS 방식은 쓰기 작업에서 충돌이 발생할 때만 재시도가 필요하므로, 읽기 작업에서는 거의 오버헤드가 발생하지 않습니다.
  • 쓰기 작업이 많은 환경: Synchronized 방식이 더 안정적일 수 있습니다. CAS 방식은 쓰기 작업이 많을 때 충돌 빈도가 높아져 반복적인 재시도로 인해 CPU 사용률이 증가할 수 있습니다. Synchronized 방식은 이러한 충돌을 피하고 안정적으로 동기화를 보장합니다.

테스트 결과

대표적인 Synchronized 구현체인 HashTableCAS방식을 사용한 Lock-Free 방식을 사용한 ConcurrentHashMap의 속도 차이를 정리해보았습니다.

private static final int NUM_OPERATIONS = 2_000_000;
private static final int NUM_THREADS = 100;
  • 낮은 동시성 : 단일 쓰레드 환경에서 NUM_OPERATIONS 크기 만큼 연산
  • 높은 동시성 : NUM_THREADS의 개수를 가진 쓰레드 풀에서 연산
구현 방법 낮은 동시성 (ms) 높은 동시성 (ms) 읽기 위주 (ms) 쓰기 위주 (ms)
Hashtable 53 15998 5124 16168
ConcurrentHashMap 47 1755 171 1674

정리

동시성 문제가 발생되는 자료구조라면 동시성이 높건 낮건 상관없이 ConcurrentHashMap을 사용하는 것을 권장합니다.

참고

댓글남기기