
Blocking I/O 처리량을 획기적으로 높여주는 virtual thread... 쉽게 적용 가능하고 성능이 좋아진다로고만 알고있었는데 이번 기회에 제대로 파헤쳐 보려한다.
기존 platform thread(OS thread)의 한계를 해결하기 위해 Java 21에서 등장한 경량 스레드
이 virtual thread는 기존 스레드의 어떤 문제점을 해결하고자 등장한걸까?
이는 컨텍스트 스위칭 및 스케줄링이 OS 레벨에서 발생하는 것을 의미하며 컨텍스트 스위칭의 비용도 많이 들고 OS는 JVM의 애플리케이션 로직을 모르기 때문에 비효율적인 스케줄링을 할 가능성이 있다.
Thread Pool Sizing 문제
// CPU 집약적 작업: 코어 수만큼
ThreadPoolExecutor cpuPool = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(), ...);
// I/O 집약적 작업: 어떻게 설정해야 할까?
ThreadPoolExecutor ioPool = new ThreadPoolExecutor(
???, ...); // 정답이 없음
Blocking과 처리량의 역설
그럼 이러한 platform thread의 문제점을 해결한 virtual thread는 어떤 특징을 가지고 있을까?
Virtual Thread는 Platform Thread 대비 1000분의 1 수준의 메모리를 사용한다. 이는 객체 지향적 설계로 스택을 힙에 저장하고 필요에 따라 동적으로 확장
Virtual Thread는 실제 OS Thread(Carrier Thread) 위에서 실행되지만, 1:1 고정 관계가 아니다. 하나의 Carrier Thread가 여러 Virtual Thread를 시분할로 처리할 수 있어 OS 자원을 효율적으로 활용 가능하다.
virtual Thread는 선점형이 아닌 협력형 스케줄링을 사용한다. Blocking 작업(I/O, sleep 등)에서만 자발적으로 CPU를 양보하며, 이는 컨텍스트 스위칭 오버헤드를 극단적으로 줄인다.
Virtual Thread는 구조적 동시성 패러다임과 자연스럽게 결합되어 작업의 생명주기를 명확하게 관리할 수 있다. 부모-자식 관계의 작업 계층구조를 통해 예외 전파와 취소를 체계적으로 처리한다.
좀 더 virtual thread가 어떻게 동작하길래 높은 동시 처리량을 달성할 수 있는지 알아보자

[출처] : https://youtu.be/AuBHv8NOca4?si=8EH0LkGQ39f3aj-J
virtual thread의 컨텍스트는 사실 jvm의 platform thread의 메모리 영역을 조작하는 것이다. 즉, OS kernel이 아닌 application level에서 컨텍스트 스위칭이 일어나는 것이고 이는 Blocking I/O 처리량을 높이는 주된 이유이다.

[출처] : https://youtu.be/AuBHv8NOca4?si=8EH0LkGQ39f3aj-J
platform thread는 자신의 스택과 수행할 바이트 코드의 주소를 저장하는 PC Register를 갖고 있다.
virtaul thread의 작업은 heap에 저장되어 있는데 platform thread가 virtual thread를 실행하기 위해서 heap 저장된 스택을 자신의 스택영역에 복사한다.
그리고 virtual thread가 실행해야할 바이트 코드의 주소를 PC Register에 설정한다.
지금 까지의 내용을 정리해보자면 virtual thread의 실행은 platform thread가 virtrual thread의 Runnable run() 메소드를 호출는 것!
우리가 일반적으로 알고있는 Runnable의 run()은 한번 실행하면 완료 까지 멈출 수 없다. 그런데 virtual thread는 어떻게 중단에 멈출 수 있는 것 일까?
java는 continuation을 통해 중단과 재개 매커니즘을 구현할 수 있다.
다음 예제를 통해 쉽게 이해 해보도록 하자

이 main의 결과물은 어떻게 될까?
hello!
virtual thread!
spring camp 2025!

먼저 cont의 run에 의해 greeting()이 실행이 된다.
이때 hello!가 출력이 된다.

그리고 coninuation의 yield 함수를 만나면 더이상 greeting()을 실행하지 않고 이전 main 함수로 돌아간다.
이때 virtual thread!가 출력이된다.

그리고 다시 cont.run()이 실행되면 greeting()의 yield 그 다음부터 실행된다.
따라서 마지막으로 spring camp 2025!가 출력
이렇게 run과 yield가 진행될 떄 의 stack 과 heap의 상태를 자세히 봐보도록 하자

greeing()의 continuation.yield()가 실행될 때의 stack과 heap의모습이다. yield에 의해 cont.run() 부터 yield 함수까지의 stack frame들이 stack chunk로 만들어져 heap에 복사가 된다.


복사가 된 이후에는 해당 stack frame들이 stack에서 제거가 된다. 이때의 상황이 마치 main 함수로 되돌아가는 상황처럼 보여진다.


그리고 다시 cont.run()을 만나게 되면 heap에 복사 해두었던 stack frame을 stack 영역으로 다시 복원시킨다.
이때의 상황이 마치 중단된 작업을 재개시키는 상황처럼 보여진다.


복원이 완료되면 yield 이후의 함수 남아있던 명령어 부터 실행한다.

virtual thread의 상태는 크게 실행, 중단, 재개로 나뉘어진다.

virtual thread 내부의 cont는 이전에 살펴봤던 continuation이고 runContinuation은 virtual thread의 runnable을 실행시키는 runnable이다.


3가지 상태중 실행 상태로 만드는 start 함수를 보면 submitRunContinuation이 있는데 이것은 runContinuation Runnable을 스케줄러에 등록하는 것이다.



runContination을 보면 mount와 continuation run이 보이는데 각각 다음과 같은 역할을 한다.
mount : virtual thread와 platform thread간의 관계를 맺어준다.
run : virtual thread에 할당된 작업들이 실행된다


중단의 상태로 만드는 park 함수를 보면 yieldContinuation이 있는데 이는 Coninuation yield를 호출한다.

heap에 stack frame을 복사하고 stack에서 해당 stack frame을 삭제함으로 코드의 흐름은 finally 부분을 실행하는 것이다.

그리고 재개 상태인 unpark를 보면 start와 마찬가지로 runConinuation을 스케줄러에 등록하는 것이다.
virtual thread의 동작원리 부분은 springcamp by KSUG에 올라온 Virtual Thread 어디까지 보고 오셨어요? (박성훈, 정승주)의 영상을 요약 및 정리 한 것이다. 해당 영상은 continuation, virtual thread를 잘 몰랐던 필자도 정말 쉽게 이해할 정도로 잘 만들어진 발표자료, 발표이다. 꼭 영상을 보는 것을 강추한다!
참고로 커널 레벨에서 Blocking I/O가 발생하면 OS thread를 Blocking 해버리는데 이는 platform thread가 Blocking 되버리는 것과 마찬가지이다. (virtual thread의 실행은 사실상 platform thread가 수행하는 것인데 이것이 blocking 되버린다는 뜻) 이러한 문제를 해결하기 위해 virtual thread는 어떤 방법을 채택했는지도 영상에 나오는데 이 내용또한 정말 흥미로우니 영상을 끝까지 보는 것을 추천한다
[스프링캠프 2025 [Track 2] 4. Virtual Thread 어디까지 보고 오셨어요? (박성훈, 정승주)] : https://youtu.be/AuBHv8NOca4?si=8EH0LkGQ39f3aj-J