🧵 스레드(Thread) 완전 정복
"프로세스가 공장이라면, 스레드는 그 안에서 일하는 작업자다."
🔵 스레드란?
스레드(Thread) = 프로세스 내에서 실행되는 독립적인 흐름의 단위
OS가 CPU 스케줄링을 통해 실행 시간을 할당하는 가장 작은 단위다.
| 시대 | 방식 | 특징 |
|---|---|---|
| 과거 | 단일 스레드 (Single-threaded) | 프로세스 하나 = 작업 하나 |
| 현재 | 멀티 스레드 (Multi-threaded) | 프로세스 하나 = 작업 여러 개 동시 처리 |
🏗️ 공유 vs 독립 — 핵심 구조
스레드를 이해하는 가장 중요한 질문은 "무엇을 공유하고, 무엇을 따로 갖는가?" 다.
📦 공유하는 것 (Shared)
같은 프로세스의 스레드들은 아래 영역을 함께 사용한다.
| 영역 | 내용 |
|---|---|
| Code | 실행할 프로그램 코드 |
| Data | 전역 변수, static 변수 |
| Heap | new로 생성한 객체, 동적 할당 메모리 |
🔒 독립적으로 가지는 것 (Private)
각 스레드가 자신만의 실행 흐름을 유지하기 위해 반드시 따로 가져야 하는 것들이다.
| 항목 | 역할 |
|---|---|
| Stack | 함수 호출 시 지역 변수 · 리턴 주소 저장 |
| PC (Program Counter) | 현재 실행 중인 코드 위치 추적 |
| Register Set | 계산 중인 중간 값 보관 |
💡 Stack이 독립적이기 때문에 각 스레드는 서로 다른 함수를 동시에 호출해도 충돌 없이 작동한다.
✅ 스레드를 사용하는 이유
1. 자원 효율성
프로세스 생성은 메모리 전체를 새로 할당하는 무거운 작업이다. 반면 스레드는 Code/Data/Heap을 공유하므로 Stack과 PC만 추가하면 된다. 그래서 스레드를 Lightweight Process(경량 프로세스) 라고 부르기도 한다.
2. 응답성 (Responsiveness)
웹 브라우저에서 파일을 다운로드하면서 동시에 다른 탭을 탐색할 수 있는 이유가 바로 멀티스레딩 덕분이다.
Thread A → 파일 다운로드 중...
Thread B → UI 렌더링 처리 중...
Thread C → 백그라운드 캐시 갱신 중...
3. 데이터 공유의 용이성
프로세스 간 통신(IPC)은 복잡하고 느리지만, 스레드는 Heap을 공유하므로 데이터 교환이 훨씬 빠르고 간단하다.
| | 프로세스 간 통신 (IPC) | 스레드 간 통신 | |--|------------------------|----------------| | 방식 | 소켓, 파이프, 공유 메모리 등 | Heap 직접 공유 | | 속도 | 느림 | 빠름 | | 복잡도 | 높음 | 낮음 |
⚔️ 양날의 검 — 동기화 문제
공유는 편리하지만, 동시에 접근할 때 충돌이 발생한다.
🚨 Race Condition (경쟁 상태)
// 두 스레드가 동시에 count++ 실행 시
int count = 0;
Thread A: count 읽기 (0) → +1 → 저장 (1)
Thread B: count 읽기 (0) → +1 → 저장 (1) ← A의 결과 덮어씀!
// 기대: count = 2
// 실제: count = 1 ← 데이터 손상!
🔑 Java의 동기화 해결책
1. synchronized 키워드 — 메서드나 블록에 락(Lock)을 걸어 하나의 스레드만 접근 허용
public synchronized void increment() {
count++; // 한 번에 하나의 스레드만 실행
}
2. ReentrantLock — 더 세밀한 락 제어가 필요할 때
Lock lock = new ReentrantLock();
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 반드시 해제
}
3. AtomicInteger — 단순 숫자 연산에 최적화된 원자적(Atomic) 변수
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 스레드 안전한 +1
☠️ Deadlock (교착 상태)
두 스레드가 서로 상대방의 락이 풀리길 기다리다 영원히 멈추는 상태.
Thread A: 락 A 보유 → 락 B 기다리는 중...
Thread B: 락 B 보유 → 락 A 기다리는 중...
→ 둘 다 영원히 대기 → 시스템 멈춤 💀
⚠️ Deadlock 예방을 위해 락 획득 순서를 일관되게 유지하거나 타임아웃을 설정해야 한다.
☕ Java에서 스레드 사용하기
Thread 클래스 상속
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread 실행: " + Thread.currentThread().getName());
}
}
MyThread t = new MyThread();
t.start(); // run()을 별도 스레드에서 실행
Runnable 인터페이스 구현 (권장)
Runnable task = () -> {
System.out.println("Thread 실행: " + Thread.currentThread().getName());
};
Thread t = new Thread(task);
t.start();
ExecutorService — 스레드 풀 (실무 권장)
ExecutorService executor = Executors.newFixedThreadPool(4); // 스레드 4개
executor.submit(() -> System.out.println("작업 1 실행"));
executor.submit(() -> System.out.println("작업 2 실행"));
executor.shutdown(); // 작업 완료 후 종료
💡 실무에서는 스레드를 직접 생성하기보다
ExecutorService(스레드 풀) 를 사용하는 것이 자원 관리 측면에서 훨씬 안전하고 효율적이다.
📝 핵심 요약
| 항목 | 내용 |
|---|---|
| 스레드 정의 | 프로세스 내 독립적인 실행 흐름의 단위 |
| 공유 영역 | Code, Data, Heap |
| 독립 영역 | Stack, PC, Register |
| 장점 | 자원 효율, 응답성, 빠른 데이터 공유 |
| 위험 | Race Condition, Deadlock |
| Java 동기화 | synchronized, ReentrantLock, AtomicInteger |
| 실무 권장 | ExecutorService(스레드 풀) 사용 |