Java Virtual Thread가 뭐임?
Java Virtual Thread에 대해 공부한 걸 정리해요

Intro
Virtual Thread는 자바 19에 Preview로 처음 도입되었고, 21부터는 Stable로 포함된 경량 스레드 기능이에요.
이 글에선 자바 Virtual Thread가 뭐가 다른지, 어떤 상황에서 쓰면 좋은지 정리할게요.
Platform Java Thread(Original)
자바에서, 기존의 Thread는 모두 Platform Thread를 사용했어요. 이는 OS(커널)수준 스레드를 감싸는 레퍼인데, 쉽게 자바의 new Thread().start()
마다 OS 스레드가 하나 생성된다고 생각해요.
1:1로 매핑되는 만큼, 생성/해제 비용이 커 Thread를 다룰 땐 대부분 Pooling 방식으로 재사용하지만..
문제는, OS스레드는 꽤 무겁기 때문에 많이 만들기 어렵고, 컨텍스트 스위칭 비용도 무시할 수 없어요.

이를 극복하기 위해서 Virtual Thread가 탄생했어요.
Virtual Java Thread
JEPS에서는 Virtual Thread에 대해 이렇게 말하고 있어요:
가상 스레드는 OS가 아닌 JDK의 가벼운 스레드 구현체이다. 이는 go의 goroutine, Erlang의 process와 같이 user-mode스레드의 한 형태이며, 많은(M) Virtual Thread가 실제(N) Platform Thread에서 스케줄링되는 M:N 스케줄링을 사용한다.
여기서 Platform Thread는 'carrier'라고 부르는데,
Virtual Thread는 이 carrier에 mount돼서 실행돼요.
그럼 blocking 발생 시에 벌어지는 일들을 간단히 살펴보면:
- 현재 실행 컨텍스트 (스택과 명령어 포인터 등)를 JVM이 저장하고 (park),
- 해당 Virtual Thread는 carrier 스레드에서 unmount되요.
- I/O가 완료되면, JVM의 스케줄러가 이를 꺼내서(unpark)
- 다른 carrier에 다시 mount해서중단된 부분부터 다시 실행해요

OS의 스레드 전환처럼 보이지만, 실제로는 JVM이 직접 흐름을 제어하기 때문에
가볍고 효율적이에요(메모리가 허락하는 한 수백만개의 스레드 생성 가능!).
참고: Platform Thread는 보통 512 KB ~ 1 MB가 기본이고,Virtual Thread는 약 2 KB가 기본이에요.
CODE
기존 자바 Thread와 동일한 API를 사용하기 때문에, 코드 자체는 거의 다를게 없어요.
단일 스레드 생성
// 단일 스레드 생성
Thread.startVirtualThread( ()->{...})
Thread.ofVirtual().name("myVirtual").start( ()->{...});
Executor(Service) 생성
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
executorService.submit( () -> {...})
}
//이름붙이기
ThreadFactory factory = Thread.ofVirtual().name("myVirtual-", 0).factory();
try (ExecutorService executorService = Executors.newThreadPerTaskExecutor(factory)){
executorService.submit( () -> {...})
}
Try-with-resource로 AutoClosable을 구현해요
언제 사용해야 할까?
Virtual Thread는 (실행시간의 대부분이 block 상태인) 단순 I/O bound 작업에 적합해요.
CPU bound 작업에는 (가상)스레드를 많이 사용해도 CPU 코어에 의해 처리량이 결정되어 있기 때문에, Platform Thread에 비해 이점이 없고, 내부 스케줄링 오버헤드만 생길 수 있어요.
이전엔 I/O가 많더라도, Platform Thread의 부담 때문에 Thread Pool을 제한적으로 사용할 수밖에 없었지만, 이젠 Virtual Thread 덕분에 필요할 때 마다 새로 만들어 쓰는 구조가 가능해요.
위에서 설명했듯 기존 Thread와 동일한 API를 사용하기 때문에, 기존 코드에 도입하기도 편하죠
물론 주의할 점도 있는데...
주의해야될 점!!!!
Daemon
Virtual Thread는 항상 Daemon으로 동작해요. 즉, join()
,awaitTermination()
등으로 제어하지 않으면 JVM이 먼저 종료될 수 있어요.
기존 ThreadPool 방식 ❌
기존 Thread는 생성이 무거워 항상 pooling해서 재사용했지만, Virtual Thread는 필요할 때마다 하나씩 만들어서 버리게 설계되었어요.
따라서, 아래 같은 코드는 병렬성을 스스로 제한하는 거예요:
ThreadFactory factory = Thread.ofVirtual().name("vthread-", 0).factory();
ExecutorService executor = Executors.newFixedThreadPool(10, factory);
Virtual Thread를 ThreadPool로 제한하는 전형적 안티패턴
동시에 실행되는 Thread 수를 제한해야 할 경우엔 다음과 같이 세마포어를 사용해야 해요:
Semaphore sem = new Semaphore(10);
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()
sem.acquire();
try {
executor.submit( () -> {...})
} finally {
sem.release();
}
10개 이상 동시에 실행되지 않음을 보장해요
ThreadLocal 사용 시 주의
Virtual Thread도 기존 Platform Thread처럼 ThreadLocal을 동일하게 지원해요.
그래서 기존 코드 대부분은 그대로 동작하지만,
Virtual Thread는 훨씬 많이 생성되고, 수명이 짧아서 ThreadLocal에 무거운 객체를 넣어두면 메모리 압박을 받을 수 있어요.
당연히 DB 커넥션 같은 자원을 ThreadLocal에서 풀링도금지!
공식 문서에선 가능한 한 ThreadLocal 사용을 지양하고, 정말 필요할 경우 ScopedValue를 사용하라고 권장하고 있어요.
JDK 자체도 Virtual Thread 도입에 맞춰서 java.base 모듈 내 대부분의 ThreadLocal 사용을 제거했다고 해요.
Pinned
Virtual Thread는 Platform Thread에 mount,unmount되면서 여러개가 동시에 실행되어야 하는데, 특정 Platform Thread에 pinned(고정)되어 unmount가 불가능한 경우가 있어요.
이러면 해당 PlatForm Thread가 다른 Virtual Thread를 처리하지 못하게 되어 병렬성이 저하되죠.
native (FFI)
어찌보면 당연한데, VM은 native 코드(FFI)실행은 가능해도 제어할 수 없기 때문에, 실행되는 동안 Virtual Thread를 스케줄링하거나 unmount할 수 없어요.
따라서 Virtual Thread는 native 코드가 끝날때까지 pinned 상태로 남아요.
synchronized
synchronized는 Platform thread에서만 관리되기 때문에, Virtual Thread가 synchronized블록을 벗어날 때까지 pinned 상태로 남아요. (공식문서에선 추후에 제거될 거래요.)
대안으로 ReentrantLock
을 사용하라고 권장하고 있어요
Lock lock = new ReentrantLock();
lock.lock();
try {
IoTask();
} finally {
lock.unlock();
}
보너스
JVM 실행 시 아래 옵션을 추가하면,
Virtual Thread가 pinned 상태가 되는 순간을 로그로 감지할 수 있어요:
-Djdk.tracePinnedThreads=full
Closing
Virtual Thread는 기존 Platform Thread를 사용하던 방식에서 거의 그대로 도입이 가능하지만 , 무턱대고 사용하다가는 여러 문제를 마주할 수 있어요.
하지만 정확히 이해하고 사용한다면, 자바의 동시성 효율을 크게 끌어올릴 수 있는 강력한 기술이에요.
이 글을 작성하면서 찾아본 문서 중 도움이 되었던 링크들을 첨부할게요.
- 공식문서 (Oracle, JEP)
- 우아한형제들, 카카오페이
- Taras Ivashchuk-Switching to Virtual Threads(Medium)
3-points
- Virtual Thread는 매우 강력한 동시성 도구이고, 도입하기도 쉽다
- 공부하지 않고 마음대로 사용하면 여러 문제가 생길 수 있다.
공부해야할게 너무 많다.
P.S 다음글로 스프링에서의 Virtual Thread에 대해 정리할 거에요.