반응형
지난 글인 [Java] Virtual Thread 간단히 알아보기 에서 가상 스레드가 도입된 배경과, 가상 스레드는 무언인가에 대해 간단하게 살펴보았다. 이번 글에서는 VirtualThread 클래스를 살펴보고, 가상 스레드가 어떤 식으로 동작하는지 코드를 통해 대략적으로 살펴보려고 한다. (참고 코드: jdk21u)
VirtualThread 클래스 살펴보기
아래는 VirtualThread 클래스의 코드 일부를 가져와보았다. 여러 멤버 변수들이 있는데 어떤 역할을 하는지 살펴보자.
final class VirtualThread extends BaseVirtualThread {
// scheduler and continuation
private final Executor scheduler;
private final Continuation cont;
private final Runnable runContinuation;
// virtual thread state, accessed by VM
private volatile int state;
// carrier thread when mounted, accessed by VM
private volatile Thread carrierThread;
// ...
}
Executor scheduler;
- 현재 작업을 수행하는 가상 스레드를 캐리어 스레드와 연결
- 디폴트로 ForkJoinPool 사용 (생성자 로직 참고)
VirtualThread(Executor scheduler, String name, int characteristics, Runnable task) {
// ...
if (scheduler == null) {
// 현재 실행중인 스레드가 가상 스레드면 해당 스레드의 스케줄러 사용
Thread parent = Thread.currentThread();
if (parent instanceof VirtualThread vparent) {
scheduler = vparent.scheduler;
} else {
// 그 외에는 디폴트 스케줄러 사용 -> ForkJoinPool
scheduler = DEFAULT_SCHEDULER;
}
}
this.scheduler = scheduler;
// ...
}
Continuation cont;
- 작업을 중단(suspend) 했다가 재개(resume)할 수 있는 구조
- 중단 상태가 되면, 호출 스택을 포함한 실행 컨텍스트를 Continuation 객체 내부에 저장
- 재개 시 마지막 중단 시점부터 수행
- VThreadContinuation 저장 (VirtualThread 생성자 로직 참고)
- 실행해야 하는 작업(Runnable)이 VirtualThread.run()을 호출하도록 구성 (wrap 메서드 참고)
VirtualThread(Executor scheduler, String name, int characteristics, Runnable task) {
// ...
this.cont = new VThreadContinuation(this, task);
// ...
}
/**
* 가상 스레드가 실행되는 continuation
*/
private static class VThreadContinuation extends Continuation {
VThreadContinuation(VirtualThread vthread, Runnable task) { ... }
@Override
protected void onPinned(Continuation.Pinned reason) { ... }
private static Runnable wrap(VirtualThread vthread, Runnable task) {
return new Runnable() {
@Hidden
public void run() {
// VirtualThread의 run 메서드로 task를 감싼다.
vthread.run(task);
}
};
}
}
Runnable runContinuation;
- Continuation cont의 run 메서드를 실행
- 실행 전후에 가상 스레드의 상태 변경이나 mount() 등의 작업 수행
VirtualThread(Executor scheduler, String name, int characteristics, Runnable task) {
// ...
this.runContinuation = this::runContinuation;
}
/**
* Runs or continues execution on the current thread. The virtual thread is mounted
* on the current thread before the task runs or continues. It unmounts when the
* task completes or yields.
*/
@ChangesCurrentThread
private void runContinuation() {
// the carrier must be a platform thread
if (Thread.currentThread().isVirtual()) {
throw new WrongThreadException();
}
// 상태 변경 작업 ...
mount();
try {
cont.run();
} finally {
unmount();
if (cont.isDone()) {
afterDone();
} else {
afterYield();
}
}
}
int state;
- 가상 스레드의 현재 상태를 저장
- 스레드의 생명 주기 및 실행 상태 등을 확인 가능
Thread carrierThread;
- 가상 스레드를 실행 중인 플랫폼 스레드를 참조하는 변수
start()
가상 스레드의 start()를 호출하면 어떤 일들이 일어나는지 코드와 함께 살펴보자. VirtualThread의 start() 메서드의 코드를 그대로 가져와 주석을 추가했다.
@Override
void start(ThreadContainer container) {
// 1. 현재 스레드의 상태를 NEW에서 STARTED로 변경
if (!compareAndSetState(NEW, STARTED)) {
throw new IllegalThreadStateException("Already started");
}
// 2. 스레드 컨테이너 세팅
assert threadContainer() == null;
setThreadContainer(container);
boolean addedToContainer = false;
boolean started = false;
try {
container.onStart(this);
addedToContainer = true;
// 3. scoped value 바인딩 상태 전파
inheritScopedValueBindings(container);
// 4. 스케줄러에 작업 제출
submitRunContinuation();
started = true;
} finally {
if (!started) {
afterDone(addedToContainer);
}
}
}
private void submitRunContinuation() {
try {
scheduler.execute(runContinuation);
} catch (RejectedExecutionException ree) {
submitFailed(ree);
throw ree;
}
}
@ChangesCurrentThread
private void runContinuation() {
...
cont.run(); // 작업 실행
...
}
- 스레드의 상태 NEW -> STARTED 변경
- 가상 스레드는 재사용 불가능하게 설계되었기 때문에 NEW 상태가 아닌 경우에는 예외 발생
- A thread can be started at most once. In particular, a thread can not be restarted after it has terminated.
스레드는 최대 한 번만 시작할 수 있으며 종료된 후에는 다시 시작할 수 없다 (Oracle Docs)
- 스레드 컨테이너 세팅
- 컨테이너: 스레드의 생명주기나 실행 환경 등을 관리할 수 있는 추상화 단계
- 컨테이너: 스레드의 생명주기나 실행 환경 등을 관리할 수 있는 추상화 단계
- scoped value 바인딩 상태 전파
- 인자로 넘긴 컨테이너를 이용해 scoped value 바인딩 상태를 물려받기
- 인자로 넘긴 컨테이너를 이용해 scoped value 바인딩 상태를 물려받기
- 스케줄러에 작업 제출
- runContinuation을 스케줄러에 제출(submit)
- 스케줄러는 별도 지정하지 않았을 경우 ForkJoinPool를 사용
- 이 runContinuation은 스케줄러에 의해 실행되는데, cont.run() 호출을 확인 가능
runContinuation()
start()를 살펴보며 작업이 스케줄러에 제출되고, runContinuation()을 통해 작업이 실행됨을 확인했다. 이제 runContinuation()을 살펴보자.
@ChangesCurrentThread
private void runContinuation() {
// 1. 플랫폼 스레드인지 확인
if (Thread.currentThread().isVirtual()) {
throw new WrongThreadException();
}
// 2. 상태 변경
int initialState = state();
if (initialState == STARTED || initialState == UNPARKED || initialState == YIELDED) {
// 상태 RUNNING으로 변경
if (!compareAndSetState(initialState, RUNNING)) {
return; // 상태 RUNNING으로 변경 실패 시 메서드 종료
}
// 상태가 UNPARKED이었다면 park permit을 false로 변경
if (initialState == UNPARKED) {
setParkPermit(false);
}
} else {
// 상태가 STARTED, UNPARKED, YIELDED가 아닌 경우 메서드 종료
return;
}
// 3. mount
mount();
try {
// 4. 작업 실행
cont.run();
} finally {
// 5. unmount
unmount();
// 6. 후처리
if (cont.isDone()) {
afterDone();
} else {
afterYield();
}
}
}
1. 플랫폼 스레드인지 확인
- 캐리어 스레드는 플랫폼 스레드에서만 사용 가능
- 현재 스레드가 가상 스레드인 경우 예외 발생
if (Thread.currentThread().isVirtual()) {
throw new WrongThreadException();
}
2. 상태 변경
- 상태가 STARTED, UNPARKED, YIELDED인 경우
- 상태 RUNNING으로 변경
- 기존 상태가 UNPARKED였다면, park permit이 사용 완료되었다고 표시 -> setParkPermit(false);
- 그 외의 상태인 경우 return;
int initialState = state();
if (initialState == STARTED || initialState == UNPARKED || initialState == YIELDED) {
if (!compareAndSetState(initialState, RUNNING)) {
return;
}
if (initialState == UNPARKED) {
setParkPermit(false);
}
} else {
return;
}
3. mount() 실행
- 플랫폼 스레드를 가상 스레드의 캐리어 스레드로 지정
@ChangesCurrentThread
@ReservedStackAccess
private void mount() {
// notify JVMTI before mount
notifyJvmtiMount(/*hide*/true);
// sets the carrier thread
Thread carrier = Thread.currentCarrierThread();
setCarrierThread(carrier);
// sync up carrier thread interrupt status if needed
if (interrupted) {
carrier.setInterrupt();
} else if (carrier.isInterrupted()) {
synchronized (interruptLock) {
// need to recheck interrupt status
if (!interrupted) {
carrier.clearInterrupt();
}
}
}
// set Thread.currentThread() to return this virtual thread
carrier.setCurrentThread(this);
}
4. 작업 실행
- cont.run()을 호출해 작업 실행
- cont는 VirtualThread 생성자에서 VThreadContinuation로 감싸짐
- cont.run()을 실행하면 가상스레드의 run() 메서드 실행
@ChangesCurrentThread
private void runContinuation() {
// 1. 플랫폼 스레드인지 확인
if (Thread.currentThread().isVirtual()) {
throw new WrongThreadException();
}
// 2. 상태 변경
int initialState = state();
if (initialState == STARTED || initialState == UNPARKED || initialState == YIELDED) {
// 상태 RUNNING으로 변경
if (!compareAndSetState(initialState, RUNNING)) {
return; // 상태 RUNNING으로 변경 실패 시 메서드 종료
}
// 상태가 UNPARKED이었다면 park permit을 false로 변경
if (initialState == UNPARKED) {
setParkPermit(false);
}
} else {
// 상태가 STARTED, UNPARKED, YIELDED가 아닌 경우 메서드 종료
return;
}
// 3. mount
mount();
try {
// 4. 작업 실행
cont.run();
} finally {
// 5. unmount
unmount();
// 6. 후처리
if (cont.isDone()) {
afterDone();
} else {
afterYield();
}
}
}
5. unmount() 실행
- 플랫폼 스레드를 캐리어 스레드에서 제거 (=가상 스레드와의 연결 관계 제거)
@ChangesCurrentThread
@ReservedStackAccess
private void unmount() {
// set Thread.currentThread() to return the platform thread
Thread carrier = this.carrierThread;
carrier.setCurrentThread(carrier);
// break connection to carrier thread, synchronized with interrupt
synchronized (interruptLock) {
setCarrierThread(null);
}
carrier.clearInterrupt();
// notify JVMTI after unmount
notifyJvmtiUnmount(/*hide*/false);
}
6. 후처리
- 작업이 완료되었다면, 상태를 TERMINDATED로 변경하고, 자원을 정리하는 등의 후속 로직 실행 (afterDone)
- 가상 스레드의 일시 중단되어 다시 스케줄링해야 할 경우 상태를 YIELDED, PARKED로 변경 등의 후속 로직 실행 (afterYield)
if (cont.isDone()) {
afterDone();
} else {
afterYield();
}
마무리
이 글을 통해 VirtualThread의 run()이 수행되면 어떤 일들이 일어나는지 코드를 통해 살펴보았다. 다음에는 park, mount 등의 작업을 수행하면 어떤 일이 일어나는지 더 자세히 살펴보고자 한다.
참고
- Oracle Docs - Virtual Threads
- 라인 블로그 - Java 가상 스레드, 깊이 있는 소스 코드 분석과 작동 원리
- 우아한기술블로그 - Java의 미래, Virtual Thread
- Project Loom: Fibers and Continuations for the Java Virtual Machine
- JEP 444: Virtual Threads
반응형
'Develop > Java+Kotlin' 카테고리의 다른 글
[Java] Virtual Thread 간단히 알아보기 (0) | 2025.03.30 |
---|---|
[Kotest] Kotest 활용 간단 가이드 (0) | 2024.10.27 |
[Java] compiler message file broken 에러 (0) | 2024.03.17 |
[Mockito] Invalid use of argument matchers! 에러 (0) | 2023.09.27 |
[Java] UnaryOperator란? (0) | 2023.06.26 |