스레드는 왜 필요한가?
과거 CPU는 한 번에 하나의 동작만 처리할 수 있었습니다.
그러나 사용자는 음악을 들으면서 웹 검색을 하거나, 영상 통화 중에 사진을 전송하는 등 여러 작업을 동시에 실행하기를 원합니다.
이를 실현하기 위한 방법이 바로 멀티스레드입니다.
멀티스레드는 하나의 프로그램에서 여러 작업을 동시에 처리할 수 있도록 해주는 기술로, 자바를 포함한 현대의 언어 대부분에서 기본적으로 지원됩니다.
프로세스와 스레드
- 프로세스(Process): 실행 중인 하나의 프로그램입니다.
- 스레드(Thread): 프로세스 내에서 실제로 작업을 수행하는 실행 흐름입니다.
하나의 프로세스는 여러 개의 스레드를 가질 수 있으며, 여러 작업을 동시에 처리하기 위해 스레드를 사용합니다. 예를 들어, 음악 앱에서 한 스레드는 노래 재생을, 또 다른 스레드는 UI 갱신을 담당할 수 있습니다.
스레드는 메모리를 어떻게 사용하는가?
스레드는 아래와 같은 메모리 구조를 가집니다:
- 공유하는 영역
- 코드 영역: 프로그램의 실행 코드
- 힙 영역: 객체가 저장되는 공간
- 독립적인 영역
- 스택 영역: 각 스레드의 지역 변수와 메서드 호출 정보
스레드는 힙에 있는 객체는 공유하지만, 스택은 각각 독립적으로 유지됩니다. 이는 스레드 간 자원 충돌의 가능성을 내포하기 때문에, 스레드 안전(safety)이 중요한 이슈가 됩니다.
스레드의 생명주기
스레드는 아래와 같은 상태를 거치며 실행됩니다:
- NEW: 스레드가 생성된 상태 (start() 호출 전)
- RUNNABLE: 실행 준비 완료, CPU 할당 대기 중
- RUNNING: CPU가 할당되어 실제 실행 중
- WAITING / TIMED_WAITING: 대기 상태 (sleep, wait 등)
- TERMINATED: 실행 종료
이러한 상태 전이 과정은 JVM과 OS가 함께 관리합니다.
**참고로 자바의 스레드와 os 스레드는 1:1 매핑 관계를 가집니다.**
멀티스레드 환경에서 발생할 수 있는 문제들
1. 가시성 문제 (Visibility Problem)
한 스레드가 변경한 공유 변수의 값이 다른 스레드에게 바로 보이지 않는 문제를 말합니다. 이는 CPU 캐시, 스레드 로컬 캐시 등으로 인해 메모리의 일관성이 깨지면서 발생합니다.
해결 방법:
- volatile 키워드
static volatile boolean flag = false;
변수에volatile을 선언하면 모든 스레드는 해당 변수 값을 메인 메모리에서 직접 읽고 쓰도록 강제합니다. 이를 통해 최신 값을 항상 볼 수 있게 됩니다.
- synchronized 키워드
synchronized(lock) { flag = true; // 변경됨 → 다른 스레드가 볼 수 있음 }
synchronized 블록 안의 코드는 락(lock)을 획득해야 실행할 수 있으며, 락을 획득하거나 해제할 때 JVM은 메모리를 동기화합니다. 이로써 가시성과 더불어 원자성까지 확보할 수 있습니다.
2. 레이스 컨디션 (Race Condition)
여러 스레드가 동일한 데이터를 동시에 수정하려고 할 때 발생하는 문제입니다. 실행 순서에 따라 결과가 달라질 수 있어 매우 위험합니다.
예: 동시에 출금 요청을 처리하는 코드에서, 두 스레드가 같은 계좌의 잔고를 동시에 읽고 각각 갱신하면 데이터가 꼬일 수 있습니다.
스레드 A: withdraw(100)
스레드 B: withdraw(200)
1. A: balance 읽음 → 1000
2. B: balance 읽음 → 1000
3. A: balance - 100 → 900
4. B: balance - 200 → 800
5. A: balance에 900 저장
6. B: balance에 800 저장 ← 💥 여기서 문제 발생!
결과: 잔고가 900이어야 하는데 800이 됨 → 이걸 RaceCondition 이라고 합니다, 누가 먼저 저장하느냐에 따라 결과가 바뀜 (비결정적)
해결 방법:
- synchronized 키워드를 사용해 코드에 락을 걸고, 한 번에 하나의 스레드만 접근하게 제한합니다.
- 객체에 락을 걸어 스레드 간의 접근 충돌을 방지합니다. 예를 들어, synchronized 메서드는 해당 객체 전체에 락을 걸며, synchronized 블록은 특정 객체에만 락을 겁니다.
그럼 락만 걸면 동시성 문제는 다 해결될까?
- 장점
- 데이터 일관성 유지
- Race Condition 방지
- 단점
- 성능 저하 (한 번에 한 스레드만 실행)
- 데드락(Deadlock) 위험
데드락 문제가 발생할 수 있습니다.
데드락이란?
데드락은 두 개 이상의 스레드가 서로 자원을 점유하고, 상대방이 가진 자원을 기다리는 상황으로, 모든 스레드가 멈추는 심각한 상태입니다.
데드락이 발생하기 위한 조건은 다음 네 가지입니다:
- 상호 배제 (Mutual Exclusion)
자원은 한 번에 하나의 스레드만 점유 가능 - 점유 대기 (Hold and Wait)
자원을 점유한 채 다른 자원을 기다림 - 비선점 (No Preemption)
자원을 강제로 뺏을 수 없음 - 환형 대기 (Circular Wait)
서로가 가진 자원을 순환적으로 기다리는 구조
이 조건 중 하나라도 깨면 데드락은 발생하지 않습니다.
데드락 해결 방법 예시:
- 락 획득 순서를 항상 동일하게 유지
- 락 타임아웃 설정 (예:
tryLock(timeout)) - 운영체제 수준에서 데드락 감지 알고리즘 사용
정리
스레드는 프로그램 내에서 실행 흐름을 나누어 병렬 작업을 가능하게 해줍니다. 그러나 멀티스레드 환경에서는 가시성 문제, 동기화 이슈, Race Condition, 데드락 같은 문제들이 발생할 수 있으므로 반드시 적절한 동기화 도구(volatile, synchronized, Lock, Atomic 등)를 통해 안정적으로 제어해야 합니다.
멀티스레드는 성능을 향상시킬 수 있는 강력한 도구이지만, 그만큼 복잡성과 책임도 따릅니다. 동시성을 설계할 때는 항상 "공유 자원", "실행 순서", "락의 범위와 순서"에 신경 써야 합니다.
참고문헌
'JAVA' 카테고리의 다른 글
| G1 GC vs CMS GC (2) | 2025.07.26 |
|---|---|
| GC(Garbage Collection) (2) | 2025.07.26 |
| Stack vs Heap (1) | 2025.07.10 |
| JMM (0) | 2025.07.07 |
| JIT(Just-In-TIme) Complination (4) | 2025.07.07 |