본문 바로가기
Develop/Java+Kotlin

[Java] VirtualThread 동작 살펴보기

by 연로그 2025. 6. 1.
반응형

 

지난 글인 [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(); // 작업 실행
    ...
}

 

  1. 스레드의 상태 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)

  2. 스레드 컨테이너 세팅
    • 컨테이너: 스레드의 생명주기나 실행 환경 등을 관리할 수 있는 추상화 단계

  3. scoped value 바인딩 상태 전파
    • 인자로 넘긴 컨테이너를 이용해 scoped value 바인딩 상태를 물려받기

  4. 스케줄러에 작업 제출
    • 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 등의 작업을 수행하면 어떤 일이 일어나는지 더 자세히 살펴보고자 한다.

 


참고

반응형