CPU가 어떻게 명령을 실행하고, 여러 쓰레드를 어떻게 번갈아 처리하는지,
그리고 Java 쓰레드가 그 위에서 어떻게 동작하는지를 흐름 중심으로 정리한 문서.
CPU(Central Processing Unit)는 명령어를 해석하고 실행하는 부품이다.
핵심 구성요소는 다음과 같다.
| 구성요소 | 역할 |
|---|---|
| ALU (산술논리장치) | 덧셈, 비교, 논리연산 등 실제 계산 수행 |
| CU (제어장치) | 명령어를 해석하고 각 부품에 신호를 보냄 |
| 레지스터 (Register) | CPU 내부의 초고속 저장 공간 (연산 중간값, 주소 등) |
| 캐시 (L1/L2/L3) | 메모리보다 빠른 임시 저장소, 자주 쓰는 데이터 보관 |
레지스터 > L1 캐시 > L2 캐시 > L3 캐시 > 메인 메모리(RAM) > 디스크(SSD/HDD)
(1 cycle) (~4) (~10) (~40) (~100+ cycle) (수만 cycle)
CPU는 매우 빠르지만 메모리는 상대적으로 느리다. 그래서 캐시가 존재한다.
CPU는 아래 사이클을 끊임없이 반복한다. 이것이 CPU가 "동작한다"의 본질이다.
┌─────────────────────────────────────────────────┐
│ │
│ ① Fetch ──▶ ② Decode ──▶ ③ Execute │
│ (인출) (해석) (실행) │
│ ▲ │ │
│ │ ▼ │
│ └────── ④ Write-back ◀───────┘ │
│ (결과 저장) │
└─────────────────────────────────────────────────┘
PC 증가 → 다음 명령어 반복
현대 CPU는 위 단계를 겹쳐서 처리해 처리량을 높인다.
시간 →
명령1: Fetch Decode Execute Write
명령2: Fetch Decode Execute Write
명령3: Fetch Decode Execute Write
한 명령이 끝나길 기다리지 않고, 앞 명령이 Decode 중일 때 다음 명령을 Fetch한다.
┌──────────────── Process ────────────────┐
│ Code Data Heap (공유) │
│ │
│ ┌ Thread 1 ┐ ┌ Thread 2 ┐ ┌ Thread 3┐│
│ │ Stack │ │ Stack │ │ Stack ││
│ │ PC/Reg │ │ PC/Reg │ │ PC/Reg ││ ← 쓰레드별 고유
│ └──────────┘ └──────────┘ └─────────┘│
└──────────────────────────────────────────┘
쓰레드는 Heap을 공유하기 때문에 데이터 공유가 쉽지만,
동시에 접근하면 동시성 문제(race condition) 가 발생한다 → 그래서 동기화가 필요.
CPU 코어 1개는 한 순간에 딱 하나의 쓰레드만 실행할 수 있다.
그런데 실행할 쓰레드는 여러 개다. 그래서 CPU는 아주 짧은 시간(예: 수 ms)마다
쓰레드를 번갈아 가며 실행한다 → 사람 눈엔 동시에 도는 것처럼 보인다.
이때 실행 중이던 쓰레드의 상태를 저장하고, 다음 쓰레드의 상태를 복원하는 작업이
바로 컨텍스트 스위칭이다.
쓰레드가 실행을 재개하는 데 필요한 모든 정보:
이 정보는 커널의 PCB(Process Control Block) / TCB(Thread Control Block) 에 저장된다.
Thread A 실행 중
│
▼
┌──────────────────┐
│ 인터럽트/타임슬라이스 만료 │ ← 트리거 발생
│ 또는 I/O 대기, yield 등 │
└──────────────────┘
│
▼
① Thread A의 컨텍스트 저장 (레지스터/PC → TCB_A)
│
▼
② 스케줄러가 다음 쓰레드 B 선택
│
▼
③ Thread B의 컨텍스트 복원 (TCB_B → 레지스터/PC)
│
▼
Thread B 실행 재개
컨텍스트 스위칭은 공짜가 아니다. 그 자체는 "일"을 하지 않는 순수 오버헤드.
쓰레드가 너무 많으면 스위칭 비용이 실제 작업보다 커지는
쓰레드 스래싱(thrashing) 이 발생할 수 있다.
"쓰레드 많다고 무조건 빠른 게 아니다."
스케줄러(OS 커널)가 "다음에 어떤 쓰레드를 실행할지" 결정한다.
| 알고리즘 | 설명 |
|---|---|
| FCFS | 먼저 온 순서대로 (First Come First Served) |
| SJF | 짧은 작업 먼저 (Shortest Job First) |
| Round Robin | 각 쓰레드에 동일한 타임 슬라이스를 순환 배분 (선점형) |
| Priority | 우선순위 높은 쓰레드 먼저 |
| MLFQ | 다단계 피드백 큐, 실사용 OS가 많이 채택 |
Java의 Thread는 대부분의 JVM(HotSpot)에서 1:1로 OS 네이티브 쓰레드에 매핑된다.
즉, new Thread().start()를 하면 실제로 OS 커널 쓰레드가 하나 생긴다.
→ 스케줄링과 컨텍스트 스위칭은 결국 OS가 담당한다.
Java Thread ──1:1──▶ OS Native Thread ──▶ CPU Core에서 실행
// 1) Runnable 구현
Runnable task = () -> System.out.println("실행: " + Thread.currentThread().getName());
Thread t = new Thread(task);
t.start(); // start()가 OS 쓰레드 생성 → run() 실행
// ※ run()을 직접 호출하면 그냥 현재 쓰레드에서 실행됨 (새 쓰레드 X)
// 2) ExecutorService (실무 권장) — 쓰레드 풀 재사용으로 생성 비용 절감
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(task);
pool.shutdown();
쓰레드 풀을 쓰는 이유: 쓰레드 생성/소멸은 비싸다.
미리 만들어 두고 재사용하면 생성 비용 + 컨텍스트 스위칭 관리가 유리하다.
같은 Heap(공유 자원)을 여러 쓰레드가 건드리면 문제가 생긴다.
class Counter {
private int count = 0;
// synchronized: 한 번에 한 쓰레드만 진입 (락 획득)
public synchronized void increment() { count++; }
}
synchronized, ReentrantLock: 상호배제(mutual exclusion)volatile: 가시성 보장 (캐시가 아닌 메인 메모리에서 읽도록)java.util.concurrent의 AtomicInteger, ConcurrentHashMap 등 활용⚠️ 락 대기는 곧 블로킹 → 컨텍스트 스위칭을 유발한다. 락 경합이 심하면 성능 저하.
// Java 21+
Thread.startVirtualThread(() -> {
// 블로킹 I/O를 해도 OS 쓰레드를 점유하지 않음
});
Thread.State enum 기준 6가지 상태:
start()
NEW ──────────▶ RUNNABLE ◀───────────────┐
│ ▲ │
synchronized │ │ 락 획득 │ notify()/
블록 진입 대기 │ │ │ 시간 만료
▼ │ │
BLOCKED │
│ │
wait()/join() │ WAITING /
sleep() └──────────────▶ TIMED_WAITING
│
run() 종료 │
RUNNABLE ─────────▶ TERMINATED
| 상태 | 의미 |
|---|---|
| NEW | 생성됨, 아직 start() 안 함 |
| RUNNABLE | 실행 중이거나 실행 가능 (CPU 배정 대기 포함) |
| BLOCKED | 모니터 락 획득을 기다림 (synchronized) |
| WAITING | 다른 쓰레드의 신호를 무한 대기 (wait(), join()) |
| TIMED_WAITING | 시간 제한이 있는 대기 (sleep(t), wait(t)) |
| TERMINATED | 실행 종료 |
RUNNABLE 안에서 OS가 실제 CPU에 올릴 쓰레드를 고르고, 그 과정에서 컨텍스트 스위칭이 일어난다.
동시성 (1 core): A B A B A B (빠르게 번갈아 → 동시처럼 보임)
병렬성 (2 core): A A A A (실제 동시 실행)
B B B B
┌── Core 0 ──┐ ┌── Core 1 ──┐
│ Thread A │ │ Thread C │
│ (실행) │ │ (실행) │ ← 진짜 병렬
└────────────┘ └────────────┘
Thread B, D ... 는 각 코어에서 컨텍스트 스위칭으로 순번 대기
| 상황 | 권장 |
|---|---|
| CPU 바운드 (계산 위주) | 쓰레드 수 ≈ 코어 수. 과하게 만들면 스위칭 손해 |
| I/O 바운드 (네트워크/디스크) | 쓰레드를 더 많이, 또는 가상 쓰레드/비동기 활용 |
| 락 경합 심함 | 락 범위 축소, lock-free 자료구조, 분할(sharding) |
| 컨텍스트 스위칭 과다 | 쓰레드 풀 크기 조정, 가상 쓰레드 검토 |