새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.
Synchronized와 CAS 연산의 차이
목표 D-day : 95 일
자바에서 제공하는 자료 구조중 HashTable과 ConcurrentHashMap의 동작 차이를 학습하려고 합니다.
학습 목표
- CAS와 Synchronized의 동작 차이
- 실제 테스트 코드로 속도 차이 확인하기
- 트레이드 오프 비교해보기
CAS와 Synchronized란
자바는 멀티 쓰레드 환경에서 동적으로 생성된 객체를 관리하는 힙 메모리 영역을 포함한 모든 메모리 영역을 공유하게 됩니다. 여러 쓰레드가 동시에 힙 메모리에 저장된 자료구조에 접근하여 데이터를 변경하려고 할 때, race condition
이 발생할 수 있습니다. 사용자가 원치 않는 결과나 상태 변경이 발생할 수 있습니다.
동시성 제어를 위한 여러가지 방법이 있습니다.
- 불변 객체 사용으로
thread-safe
하게 사용하기 - CAS방식인
Atomic
클래스 사용하기 - Synchronized 키워드 사용하기
- 그 외 ..
오늘은 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 방식은 다음과 같은 과정을 따릅니다:
- 수정하려는 데이터를 검색 후
변수 A
에 저장합니다. 변수 A
의 예상된 새 값을 계산합니다.- 같은 조건으로 데이터를 다시 검색 후
변수 B
에 저장합니다. - 변수
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
구현체인 HashTable 과 CAS
방식을 사용한 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
을 사용하는 것을 권장합니다.
댓글남기기