OS레벨에서 Context Switch가 없고, JVM 내부에서 실행 상태를 변경해주므로 빠르고 저렴한 Context Switching 가능 (약 10배 감소)
Platform Thread에 비해 소요 메모리 감소 (약 200배)
JVM heap 이 허용하는 범위 내 계속 생성 가능
mount
라고 하고, 실행이 중단되어 Carrier Thread에서 분리되는 것을 unmount
라고 한다.park()
을 실행하면 mount
되고, unpark()
을 실행하면 unmount
된다 private static final int NEW = 0;
private static final int STARTED = 1;
private static final int RUNNABLE = 2; // runnable-unmounted
private static final int RUNNING = 3; // runnable-mounted
private static final int PARKING = 4;
private static final int PARKED = 5; // unmounted
private static final int PINNED = 6; // mounted
private static final int YIELDING = 7; // Thread.yield
private static final int TERMINATED = 99; // final state
1. NEW
• 설명: 아직 시작되지 않은 가상 스레드의 초기 상태.
• Mount 상태: ❌ Unmount
• 전이: Thread.start()
호출 시 → STARTED
2. STARTED
• 설명: 시작되었지만 아직 Carrier Thread에 붙지 않은 상태. JVM이 실행 스케줄링을 준비 중.
• Mount 상태: ❌ Unmount
• 전이: 스케줄러가 스레드를 할당 → PINNED 또는 RUNNABLE
3. PINNED
• 설명: 특정 Carrier Thread에 고정된 상태. 주로 JNI 코드나 블로킹 연산 등으로 인해 가상 스레드가 해당 플랫폼 스레드에 묶인(pinned) 상태입니다.
• Mount 상태: ✅ Mount
• 전이: yield 실패 시 → RUNNING
4. RUNNING
• 설명: 실제로 CPU에서 실행되고 있는 상태 (Carrier Thread 위에서 실행 중)
• Mount 상태: ✅ Mount
• 전이: yield()
또는 작업 완료 → YIELDING, PARKING, TERMINATED
5. YIELDING
• 설명: 다른 Virtual Thread에게 CPU를 양보하려는 상태
• Mount 상태: ✅ Mount
• 전이: Thread.yield()
성공 → RUNNABLE
• 실패 → 다시 RUNNING
6. RUNNABLE
• 설명: 실행 준비가 완료되어 실행 큐에 들어가 있는 상태. 아직 Carrier Thread에 mount되지 않음. 스케줄러에 의해 적절한 Virtual Thread 가 RUNNABLE 하게 된다.
• Mount 상태: ❌ Unmount
• 전이: JVM 스케줄러(기본값은 ForkJoinPool)에 의해 → RUNNING
7. PARKING
• 설명: Virtual Thread가 park()
호출 등으로 스스로 중단하려고 하는 순간의 상태
• Mount 상태: ✅ Mount
• 전이: park()
완료 시 → PARKED
8. PARKED
• 설명: park()
에 의해 중단된 상태. 어떤 이벤트가 발생할 때까지 대기.
• Mount 상태: ❌ Unmount
• 전이: unpark()
호출 → RUNNABLE
9. TERMINATED
• 설명: Virtual Thread의 실행이 끝난 상태
• Mount 상태: ❌ Unmount
• 전이: 없음 (종료 상태)
Thread
를 상속받은 클래스이다.ForkJoinPool
이다. 이는 코드에서 쉽게 확인할 수 있다.final class VirtualThread extends BaseVirtualThread {
...
private static final ForkJoinPool DEFAULT_SCHEDULER = createDefaultScheduler(); // 여기서 기본 스케줄러 생성
private static final ScheduledExecutorService UNPARKER = createDelayedTaskScheduler();
...
ForkJoinPool
은 Java 7부터 사용 가능한 클래스로 java.util.concurrent
에 포함되어 있다. (이름에서부터 동시성 처리와 관련된 클래스임을 알 수 있다)ForkJoinPool
은 Work Stealing 전략을 사용한다는 것이다. Virtual Thread 의 장점은 JVM이 자체적으로 Virtual Thread를 스케줄링하고 Context Switching 비용이 줄어들어 효율적으로 운영할 수 있다는 것이다.
synchronized
block을 사용하는 경우synchronized
을 사용하는 경우 해당 객체의 Monitor Lock을 사용한다. 이를 기반으로 JVM이 내부적으로 스레드 간 동기화를 처리하는 것이다.synchronized
block으로 들어가는 경우 Monitor Lock이 Virtual Thread에 걸리는게 아니라, 해당 Virtual Thread가 mount 되어 있는 Carrier Thread에 걸리게 된다. 그렇기 때문에 빠르게 Virtual Thread를 mount
/ unmount
할 수 없어 성능 저하가 발생한다.synchronized
을 사용한 Lock이 많아 이를 ReentrantLock
으로 바꾸고 있는 추세이다. 간단하게 보자.ReentrantLock
을 적극적으로 사용하고, synchronized
을 제거하는 모습이다. 최신 버전은 어떻게 되어 있는지 궁금해서 찾아보았다.public interface CacheAdapterFactory<K, V> {
// ReentrantLock가 아닌 Lock 인터페이스를 사용하고 있다.
CacheAdapter<K, V> getInstance(Lock lock, String url, int cacheMaxSize, int maxKeySize);
}
class PerConnectionLRU implements CacheAdapter<String, QueryInfo> {
private final int cacheSqlLimit;
private final LRUCache<String, QueryInfo> cache;
private final Lock lock; // 여기서도 Lock 인터페이스를 사용
...
// ReentrantLock 방식을 사용 (명시적인 lock, unlock 사용)
this.lock.lock();
try {
return this.cache.get(key);
} finally {
this.lock.unlock();
}
...
}
Lock
인터페이스를 사용하는 것으로 보인다. 하지만 synchronized
는 명백히 없애고 있는 것을 볼 수 있다!!!MySQL: https://github.com/mysql/mysql-connector-j/pull/95 :
UUId: https://github.com/f4b6a3/uuid-creator/commit/3e684b1dec472b51a641bbd1762b33c9ea62bc77
synchronized
때문에 제 성능을 내지 못하고 있다. 추후 미래의 Spring 에서는 지금보다 성능이 더 좋아질 것으로 보인다!synchronized
block을 사용해도 성능 저하가 되지 않도록 코드를 개선되었다! 하지만 여전히 native method 호출 시에는 pinning 여전히 존재한다.https://d2.naver.com/helloworld/1203723
https://tech.kakaopay.com/post/ro-spring-virtual-thread/
https://openjdk.org/jeps/491