Java/동시성
Java Virtual Thread
창욱씨
2025. 7. 12. 23:51
반응형
Java Virtual Threads
Java Virtual Thread는 Project Loom의 일환으로 Java 21에서 정식 도입된 경량 스레드입니다 (JEP 444).
기존 Platform Thread의 문제
기존 Java 스레드는 OS 커널 스레드와 1:1로 매핑됩니다.
- 스레드 하나당 ~1MB 스택 메모리 소비
- OS 스레드 수에 제한 → 수천 개 이상 생성 시 성능 저하
- I/O 블로킹 시 OS 스레드가 통째로 블로킹됨
- thread-per-request 모델의 확장성 한계
Virtual Thread란?
Virtual Thread는 JVM이 관리하는 경량 스레드로, 소수의 OS 스레드(carrier thread) 위에서 M:N 스케줄링됩니다.
┌─────────────────────────────────┐
│ JVM Scheduler │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ... │ ← 수백만 개의 Virtual Threads
│ │VT1│ │VT2│ │VT3│ │VT4│ │
│ └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ │
│ └──┬───┘ └──┬───┘ │
│ ┌──┴──┐ ┌──┴──┐ │
│ │ CT1 │ │ CT2 │ │ ← 소수의 Carrier Threads (= OS Threads)
│ └─────┘ └─────┘ │
└─────────────────────────────────┘
핵심 특징
| 특성 | Carrier Thread | Virtual Thread |
|---|---|---|
| 매핑 | 1:1 (OS 스레드) | M:N (JVM 관리) |
| 메모리 | ~1MB 고정 스택 | ~수 KB (동적 증가) |
| 생성 비용 | 높음 | 매우 낮음 |
| 최대 개수 | 수천 개 | 수백만 개 |
| 블로킹 I/O | OS 스레드 블로킹 | carrier에서 unmount → 다른 VT 실행 |
| 풀링 필요 | ✅ (ThreadPool) | ❌ 불필요 |
사용법
1. 기본 생성
// 단일 Virtual Thread 실행
Thread.startVirtualThread(() -> {
System.out.println("Hello from virtual thread!");
});
// Builder 패턴
Thread vt = Thread.ofVirtual()
.name("my-vthread")
.start(() -> doWork());
2. ExecutorService 활용
// Virtual Thread 전용 Executor — 태스크마다 새 VT 생성
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 100만 개의 동시 태스크도 가능
IntStream.range(0, 1_000_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
3. Spring Boot 적용 (3.2+)
# application.properties
spring.threads.virtual.enabled=true
이 한 줄로 Tomcat/Jetty의 요청 처리가 Virtual Thread 기반으로 전환됩니다.
동작 원리: Continuation 기반
Virtual Thread가 I/O 블로킹 호출을 만나면:
1. VT의 스택(continuation)을 힙에 저장 (unmount)
2. Carrier Thread가 해방되어 다른 VT 실행
3. I/O 완료 시 VT를 다시 Carrier Thread에 mount
4. 실행 재개
기존 코드를 수정 없이 블로킹 스타일로 작성해도, 내부적으로는 논블로킹처럼 동작합니다.
주의사항
| 이슈 | 설명 |
|---|---|
| Pinning | synchronized 블록 안에서 블로킹 시 carrier thread가 해방되지 않음 → ReentrantLock으로 대체 권장 |
| ThreadLocal 남용 | VT는 수백만 개 생성 가능하므로, ThreadLocal에 무거운 객체 캐싱 시 메모리 폭증 → ScopedValue (Preview) 사용 권장 |
| CPU-bound 작업 | VT는 I/O-bound 작업에 최적화. CPU-bound는 Platform Thread + ForkJoinPool이 더 적합 |
| 풀링 금지 | VT는 생성 비용이 거의 없으므로, 풀링하면 오히려 성능 저하 |
언제 사용해야 하는가?
✅ 적합한 경우:
- 대량의 동시 I/O 작업 (HTTP 호출, DB 쿼리, 파일 I/O)
- Thread-per-request 서버 모델
- 기존 블로킹 코드를 리팩터링 없이 확장하고 싶을 때
❌ 부적합한 경우:
- CPU-intensive 연산 (행렬 곱, 암호화 등)
- 이미 비동기/리액티브로 잘 동작하는 코드
synchronized를 대량 사용하는 레거시 코드 (pinning 이슈)
반응형