먼저 생활 비유
창구 하나와 번호표 세 장
직원이 서류가 도착할 때까지 한 손님 앞에서 가만히 서 있지 않습니다. 그 손님의 서류를 대기함에 넣고 다음 번호를 부릅니다. 서류가 오면 다시 대기 번호에 올리지만, 바로 창구를 차지하는 것은 아닙니다.
2.1 · 프로세스와 동시성
1코어 CPU가 Java의 sleep·blocking 코드를 만났을 때, 거기 머무르지 않고 어떻게 다른 일을 할까요?
CPU는 sleep의 뜻을 모릅니다. JVM이 대기를 요청하고, 커널이 스레드를 실행 후보에서 빼고, 스케줄러가 다음 runnable 스레드를 고릅니다.
먼저 생활 비유
직원이 서류가 도착할 때까지 한 손님 앞에서 가만히 서 있지 않습니다. 그 손님의 서류를 대기함에 넣고 다음 번호를 부릅니다. 서류가 오면 다시 대기 번호에 올리지만, 바로 창구를 차지하는 것은 아닙니다.
컴퓨터 예제로 바꾸기
Thread A: Thread.sleep(1000);
Thread B: calculateNextJob();A의 호출 스택과 다음 실행 위치는 메모리에 보관합니다. A가 타이머 대기에 있는 동안 한 개 코어에는 B의 PC와 레지스터 문맥을 올려 B의 명령을 실행합니다.
코어는 하나만 둡니다. 아래 네 경우를 바꿔 보면서 실행 중, 실행 가능, 대기를 분리해 보세요.
정답아니요. A 스레드만 타이머 대기열로 빠지고, 한 개 코어는 B나 C를 실행합니다.
지금 이 찰나에는 A의 명령만 실행됩니다. B와 C는 동시에 실행되는 것이 아니라 실행 가능 큐에서 차례를 기다립니다.
1log("A 시작")2Thread.sleep(1000)3log("A 다시 실행")
1while (hasJob()) {2 calculateNextJob()3}
RUNNABLERUNNINGRUNNABLERUNNABLERUNNABLERUNNABLE* Java의 상태 이름과 OS 스레드 상태는 1:1로 같지 않습니다.
log("A 시작")한 순간에는 A 하나만 실행됩니다.
각 층은 자기에게 보이는 정보만 처리합니다. 현재 단계에서 주도한 층을 밝게 표시했습니다.
sleep·read·락·반복 같은 행동을 요청
Java 상태·모니터를 관리하고 네이티브 기능 호출
타이머·I/O 대기와 runnable 상태를 관리
runnable 후보 중 다음 실행 흐름을 선택
선택된 문맥의 기계 명령만 실행
이 그림은 이해를 위해 실제 나노초·밀리초 규모의 변화를 느리게 펼친 것입니다. 문맥 교환에는 비용이 있고, 정확한 선택 시점과 순서는 OS 정책·우선순위·부하에 따라 달라집니다.
전통적인 Java 스레드는 OS 스레드와 거의 1:1로 연결되므로 blocking 동안 그 OS 스레드가 잠듭니다. 하지만 CPU 코어는 다른 OS 스레드에 줄 수 있습니다.
지원되는 blocking 작업에서는 가상 스레드의 문맥을 메모리에 두고 carrier 플랫폼 스레드가 다른 가상 스레드를 실행합니다. 이후 별도 실험에서 플랫폼/가상 스레드를 나란히 비교합니다.
커널의 idle 흐름이 선택되고 CPU는 인터럽트를 기다리는 저전력 상태로 들어갈 수 있습니다. sleep 중인 A를 억지로 실행하지 않습니다.
Thread.sleep은 현재 스레드의 실행만 멈추며 보유한 모니터를 놓지 않습니다. 그래서 다른 스레드가 그 락 앞에서 막힐 수 있습니다.
wait()는 객체의 wait set에 들어가며 그 객체의 모니터를 내려놓습니다. 깨어난 뒤에도 락을 다시 얻어야 반환할 수 있습니다.
처음 보는 용어 두 개
이 절을 다 읽었다면
대기는 코드를 없애는 것이 아니라 그 스레드의 문맥을 메모리에 보관하고 실행 후보에서 잠시 빼는 일입니다. 코어는 runnable인 다른 스레드를 실행합니다.