[매일매일] 백엔드 기술 일기: 동시성 문제, 경쟁 상태를 해결하기 위해 보장해야 할 것들
멀티스레드 환경에서 자주 발생하는 문제 중 하나가 바로 경쟁 상태(Race Condition) 입니다.여러 스레드가 동시에 같은 자원에 접근해 작업하면서 실행 순서에 따라 예상치 못한 결과가 발생하는 현상이죠.그렇다면 경쟁 상태를 방지하려면 어떤 조건을 만족해야 할까요?오늘은 핵심 개념인 원자성(Atomicity) 과 가시성(Visibility) 에 대해 살펴보고, Java에서 이를 보장하는 방법을 소개합니다.
🕹️ 경쟁 상태란?경쟁 상태는 여러 스레드가 동시에 공유 자원에 접근하고, 그 실행 순서에 따라 결과가 달라지는 상황입니다.예를 들어 i++ 연산을 두 스레드가 동시에 실행하면 예상보다 적은 값으로 결과가 남는 문제를 경험할 수 있습니다.
🔍 해결을 위한 두 가지 핵심: 원자성과 가시성1️⃣ 원자성 (Atomicity)원자성은 작업이 더 이상 나눌 수 없는 하나의 단위로 실행되는 성질입니다.i++처럼 하나의 문장으로 보이는 연산도 실제로는 다음 세 단계로 나뉩니다:1. i의 현재 값을 읽음 (Read) 2. 1을 더함 (Modify) 3. 결과를 다시 i에 씀 (Write) 만약 두 스레드가 이 과정 중에 서로 개입하면 다음과 같은 일이 벌어집니다:
Thread 1
Thread 2
i 읽기 (10)
i 읽기 (10)
i+1 (11)
i+1 (11)
i에 11 쓰기
i에 11 쓰기
결과적으로 i는 두 번 증가했어야 하지만 실제로는 한 번만 증가합니다.
➡️ 원자성이 깨졌기 때문입니다.
2️⃣ 가시성 (Visibility)가시성은 한 스레드가 변경한 값을 다른 스레드가 즉시 볼 수 있는 성질입니다.현대 CPU는 각 코어마다 캐시가 있어서, 변경된 값이 메인 메모리에 바로 반영되지 않고 캐시에 머무를 수 있습니다.이 때문에 다른 스레드는 메모리에 반영되지 않은 이전 값을 계속 읽게 됩니다.➡️ 즉시 최신 상태가 보장되지 않기 때문에 문제가 됩니다.
☕ Java에서 보장하는 방법Java에서는 다양한 방법으로 원자성과 가시성을 보장할 수 있습니다.
방법
보장하는 성질
설명
synchronized
원자성 + 가시성
블록이나 메서드 단위로 임계 구역 설정
ReentrantLock
원자성 + 가시성
명시적으로 락을 걸고 해제
AtomicInteger 등
원자성 + 가시성
CAS(Compare-And-Swap) 기반 연산
volatile
가시성만
CPU 캐시 대신 메모리에서 직접 읽음
주의!volatile은 가시성만 보장합니다. 하나의 스레드가 쓰고, 다른 스레드가 읽는 경우에만 적합합니다. (읽기-수정-쓰기 연산에는 부적합)
✨ 요약✅ 경쟁 상태를 방지하려면 원자성과 가시성이 모두 필요하다.✅ 원자성: 작업이 더 이상 나눠지지 않고 실행✅ 가시성: 변경된 값이 다른 스레드에서 바로 보임✅ Java에서는 synchronized, lock, Atomic 클래스, volatile 등을 활용
📚 추가 학습 자료https://www.youtube.com/watch?v=ktWcieiNzKs
https://www.youtube.com/watch?v=sQ-G8Gd5qVU