Java 21에서 새롭게 추가된 Virtual Thread에 대해 알아봅니다.
GOAL
- Virtual Thread(가상 스레드)의 동작을 이해한다.
- Virtual Thread와 Platform Thread 간 차이를 이해한다.
- Virtual Thread의 Pin 현상에 대해 이해한다.
Virtual Thread란?
Java 21부터 JEP 444 Virtual Thread(가상 스레드)가 정식 도입되었다. 가상 스레드란 사실 JVM이 만드는 Thread 객체이다. 핵심 아이디어는 다음과 같다.
실행할 때만 Platform Thread에 올라타서 CPU를 쓰고, 대기(I/O 등)에 들어가면 JVM이 그 실행 문맥을 떼어 힙에 보관하고 준비되면 다시 Platform Thread에 붙여 이어서 실행한다.
또한, Java 객체이기 때문에 하나의 실제 스레드에 여러 가상 스레드가 붙을 수 있어 전환 비용이 낮고(Context Switch 없음), 생성 시 OS 커널이 메모리 스택 할당(보통 MB 단위), 커널 자료구조 초기화, 스케줄러 등록 등 무거운 작업을 수행하지 않는다.
Why?
왜 가상 스레드가 개발되었을까? Java의 전통적인 Platform Thread는 OS 스레드(Kernel Thread)에 1:1로 매핑되어 처리량에 물리적인 한계가 존재했다. 하지만 Virtual Thread는 OS 스레드 희소성을 제거해 더 높은 동시성을 달성할 수 있도록 설계되었다.
정리하자면 Platform Thread는 생성될 때 실제 Thread와 1:1로 매핑되어 OS가 허용하는 스레드 개수 이상은 만들지 못하지만, Virtual Thread는 단순 Java 객체 생성이기 때문에 생성 자체에는 OS 스레드와 연결이 없어 OS 스레드 개수와 상관없이 생성할 수 있다. 하지만 결국 실제로 동시에 실행될 수 있는 스레드의 개수는 물리적인 OS 스레드에 제한된다는 점은 같다.
* Java의 Blocking 모델 철학
이렇게 설계된 배경에는 Java의 Blocking 모델 철학이 담겨있다. OS 스레드 수를 늘리지 않고 처리량을 높일 수 있는 방법은 비동기도 존재한다. 하지만 "비동기 프로그래밍 스타일은 애플리케이션의 동시성 단위가 더 이상 Java 플랫폼이 기본적으로 제공하는 동시성 단위(스레드)가 아니기 때문에 Java 플랫폼과 철학적으로 충돌한다"라는 말과 함께 동기 코드의 단순함을 유지하며 문제를 해결하는 가상 스레드가 탄생한 것이다.

How?
그럼 가상 스레드는 어떻게 동작할까?
기본적인 동작은 위에서 설명한 것과 같이 OS 스레드에서 대기가 걸린 가상 스레드를 내려 다른 작업을 가능하게 하는 것이다. 가상 스레드는 Java 객체이므로 교체될 때 JVM이 Context Switch 없이 OS 스레드에서 가상 스레드의 Stack Fragment만 교체하여 커널 개입 없이 JVM 내부 스케줄러에서 처리한다.
가상 스레드가 실제로 작업을 하기 위해 붙는 플랫폼 스레드들을 캐리어 스레드(Carrier Thread)라고 부른다. 실행하기 위해 캐리어 스레드에 붙이는 것을 mount라고 하고 대기로 인해 캐리어 스레드에서 내려온 것을 unmount라고 한다. 블로킹(I/O 대기 등)이 오면 JVM이 대기가 걸린 가상 스레드를 unmount 후 carrier thread를 반환하고 다른 가상 스레드를 실행한다.
이렇게 적은 수의 캐리어 스레드(=OS Thread)에서 여러 가상 스레드들이 작업하며 대기시간을 효율적으로 사용하게 된다. Java의 객체이므로 실제 스레드가 교체되는 것만큼의 오버헤드도 없고 대기 시간을 효율적으로 관리할 수 있게 된다.
스레드간 실행 흐름은?
그럼 실행 흐름은 어떻게 저장될까? OS 스레드는 OS에 의해 실행 흐름(Context)가 저장되고 실행 스레드가 바뀔 때 문맥 교환이 일어난다. 그럼 가상 스레드는 어떻게 각 실행 흐름을 저장하고 복구할까?
정리해보면 JVM이 OS가 하드웨어 스레드를 스케줄링하듯, 제한된 수의 플랫폼 스레드 위에서 수많은 가상 스레드라는 논리적 실행 흐름과 문맥을 스케줄링하는 유저 공간의 작은 OS처럼 동작한다고 이해할 수 있다.
그럼 가상 스레드는 만능인가?
지금까지 설명한 가상 스레드를 보면 더이상 기존 플랫폼 스레드를 사용할 이유가 없는 것 같다. 생성, 컨텍스트 비용이 Platform Thread보다 훨씬 작고 하지만 가상 스레드에도 단점이 존재한다.
Pin(핀)
특정 상황에서 JVM이 가상 스레드를 unmount하지 못하는 상황이 있는데, 이것을 pin(핀)이라고 한다. 이때 가상 스레드가 carrier thread에 강제로 고정(pinned)되고 carrier thread가 대기 동안 붙잡혀 버리는 현상이다. 핀 현상이 나타나면 확장성이 떨어져 사실상 Platform Thread 처럼 동작하게 된다.
가상 스레드에서 핀 생기는 이유를 단순히 말하면 JVM이 Virtual Thread를 안전하게 unmount 할 수 없는 상황이기 때문인데,대표적인 예시로 Virtual Thread가 synchronized 블록이나 JNI 같은 OS 레벨 락 안에서 오래 머무르면 carrier thread가 해제되지 못한다.
가상 스레드가 모니터(객체 락)를 이미 보유한 상태에서 I/O 호출이나 Thread.sleep, join, park 등과 같은 블로킹을 하면 문제가 생기게 된다. JVM 입장에서는 이 스레드를 unmount시키려면 락 상태(모니터 보유)도 같이 저장/복원해야 하는데, 이건 JVM 레벨에서 안전하게 할 수가 없기 때문에 carrier thread에 붙잡아 둔 채로 블로킹하여 핀 현상이 발생한다.
* 왜 락 상태(모니터 보유)도 같이 저장/복원하는 것이 안전하지 않을까?
자바의 synchronized는 내부적으로 객체 모니터를 사용하는데, 락을 잡을 때는 보통 얇은 락(thin lock)의 스택 락 기록(lock record)과 객체 헤더의 변형(displaced header) 같은 스레드-스택에 밀접히 연결되어있다.
이때 “락을 잡고 있음”이라는 상태는 해당 캐리어 스레드의 스택 프레임과 강하게 결합되어 있고 가상 스레드를 unmount 한다는 건 “지금 실행 중인 프레임/스택을 떼어내어(중단점 저장) 나중에 다른 캐리어 스레드에서 이어붙인다”는 뜻인데, 락 메타데이터가 ‘현재 스택’과 엮여 있는 동안에는 스택을 떼어내는(continuation으로 보관하는) 작업을 안전하게 할 수 없다. 따라서 JVM은 모니터 보유 중에는 ‘스택-연계 락 상태’를 깔끔히 보존/복원할 보편적 방법이 없기 때문에 unmount를 금지하고, 그 캐리어 스레드를 대기시키게 된다.
단순히 코드를 작성할 때 Synchronized 키워드만 조심하는 것이 아닌 외부 의존성에 Synchronized 구현이 있는 경우도 조심해야한다. 실제로 많이 사용되는 MySQL 기반 JDBC 드라이버인 Connector-J 8.x버전은 synchronized 구현이 있어 이슈가 있었다. 이후 MySQL 쪽에서 인지하고 Connector-J 9.0.0 버전 릴리즈노트에 "Synchronized를 ReentrantLock으로 변경하여 VT-friendly하게 수정했다"라고 명시되어있다. 하지만 모든 Synchronized를 없앤게 아닌 몇몇 I/O 경로에서의 구현을 바꾼 것이기 때문에 사용할 때 "VT를 효율적으로 사용할 수 있는 구간인가?"를 잘 판단하는 것이 중요할 것 같다. 이건 단순히 MySQL뿐의 문제가 아니라 사용할 때 "외부 의존성이 Synchronized 구현을 사용하는가?", "이 구간에서 pin 현상이 발생하는가?"를 잘 파악해야 올바르게 사용할 수 있다.

+ jdk24부터는 synchroized를 사용해도 pin 현상이 발생하지 않도록 수정했다고 한다. 모니터(락)를 캐리어 스레드가 아니라 가상 스레드에 독립적으로 붙이도록 바꾸어 synchronized 때문에 pin 현상이 발생하는 케이스는 거의 사라졌다고 한다. 사용하려면 24이후 LTS인 25버전을 사용하는 것도 좋은 방법인 것 같다!

CPU 위주 작업
대기가 거의 없는 CPU 위주의 작업은 사실상 Platform Thread와 비슷하게 동작한다. 이런 경우엔 가상 스레드의 이점을 살리지 못하고 가상 스레드를 스케줄링하기 위한 오버헤드만 발생하여 사실상 성능 손해가 일어나게 된다.
따라서 가상 스레드는 I/O, 외부 API 호출, DB 접근 등과 같이 대기가 작업 시간의 높은 비중을 차지하는 작업에 적합하다. 대기하면서 스레드가 다른 작업을 할 수 있도록 unmount되기 때문에 높은 처리량을 확보할 수 있다. 하지만 대기 시간이 길지 않고 대부분 CPU 연산 위주의 작업인 경우 플랫폼 스레드와 다를 바가 없어지고, 오히여 가상 스레드를 스케줄링하는 오버헤드만 더 소요되기 때문에 CPU 바운드 작업, 오래 잠그는 락 같은 상황에서는 Platform Thread보다 불리할 수 있다.
참고
JEP 444: Virtual Threads
JEP 444: Virtual Threads Summary Introduce virtual threads to the Java Platform. Virtual threads are lightweight threads that dramatically reduce the effort of writing, maintaining, and observing high-throughput concurrent applications. History Virtual thr
openjdk.org
JEP 491: Synchronize Virtual Threads without Pinning
JEP 491: Synchronize Virtual Threads without Pinning AuthorPatricio Chilano Mateo & Alan BatemanOwnerAlan BatemanTypeFeatureScopeImplementationStatusClosed / DeliveredRelease24Componenthotspot / runtimeDiscussionhotspot dash dev at openjdk dot org,
openjdk.org
https://dev.mysql.com/doc/relnotes/connector-j/en/news-9-0-0.html
MySQL :: MySQL Connector/J Release Notes :: Changes in MySQL Connector/J 9.0.0 (2024-07-01, General Availability)
Changes in MySQL Connector/J 9.0.0 (2024-07-01, General Availability) Version 9.0.0 is a new GA release of MySQL Connector/J. MySQL Connector/J 9.0.0 supersedes 8.4 and is recommended for use on production systems. This release can be used against MySQL Se
dev.mysql.com
'Computer Science > Java' 카테고리의 다른 글
| 객체지향과 Java (0) | 2025.10.13 |
|---|---|
| Java의 값 복사와 JVM 구조 (0) | 2025.10.13 |
| Gradle 알아보기 (0) | 2025.09.13 |