티스토리 뷰

TIL/Java

JDK 21, Virtual Thread 정리

DandyU 2025. 1. 3. 10:41

1. Virtual Thread 소개

Virtual Thread는 JDK 21에 정식 Feature로 등록된 경량 스레드로, Java의 동시성 (Concurreny) 처리를 개선하기 위해 OpenJDK의 Project Loom에서 개발되었습니다.

 

대표적인 경량 스레드로는 Kotlin의 coroutine, Go의 goroutine 등이 있습니다.

 

이제 Java만으로도 경량 스레드의 이점을 누릴 수 있게 되었고, Virtual Thread는 어떻게 동작하고 얼마나 성능이 좋아졌는지 알아보겠습니다.

 

2. Thread와 Virtual Thread의 구조

Java의 Thread(Platform Thread)는 OS의 Kernel Thread와 1:1 관계로 매핑되어 동작하는 구조입니다.

이러한 구조는 Java에서 생성 가능한 Thread의 최대 개수가 OS에서 생성 가능한 Thread의 최대 개수를 초과할 수 없다는 의미입니다. 이것은 Spring과 같이 Thread-per-Request 구조에서는 처리할 수 있는 요청의 개수가 동일하게 제한된다는 의미이기도 합니다.

 

이 밖에도 OS의 Kernel Thread는 생성/삭제/Context Switching 과정에서 발생하는 비용이 비싸서

생성/삭제 과정이라도 비용을 줄이고자, 일정한 수의 Thread를 생성하여 재사용할 수 있도록 Thread Pool을 사용합니다.

<Thread>

Virtual Thread는 JVM내 Heap 메모리에 있는 N개의 Virtual Thread를 하나의 Carrier Thread에 돌아가면서 Mount/Unmount하여 Context Switching하는 구조입니다.

 

이러한 구조는 OS의 Kernel Thread 직접적으로 매핑되지 않고, JVM 내에서 관리되는 경량화된 스레드로

OS에 의해 최대 생성 개수를 제한 받지 않고 JVM의 가용 Heap 메모리 내에서 제한 없이 Virtual Thread를 생성할 수 있게 되었고, 생성/삭제/Context Switching 과정에서 발생하는 비용이 기존 Thread에 비해 크게 절감되었습니다.

<Virtual Thread>

3. Thread와 비교 시 Virtual Thread의 개선점

  • Thread의 생성과 관리 비용을 개선
    • OS의 Kernel Thread는 OS에 의해 생성되고 스케줄링되어 CPU에서 실행됩니다.
    • JVM은 JNI를 통해 Kernel Thread를 제어합니다.
    • 이러한 구조는 Thread의 생성/삭제/Context Switching 과정에서 많은 비용이 발생합니다.
    • Virtual Thread는 OS의 Kernel Thread 직접적으로 매핑되지 않고 JVM 내부에서 Virtual Thread의 생성/삭제/Context Switching을 직접 관리하여 비용을 절감시켰습니다.
  • 제한된 개수만 생성 가능한 Thread를 개선
    • Thread는 OS의 Kernel Thread를 1:1로 매핑한 구조입니다.
    • 이 구조는 Java에서 생성 가능한 Thread의 최대 개수가 OS에서 생성 가능한 Thread의 최대 개수를 초과할 수 없다는 의미입니다.
    • Virtual Thread는 OS의 Kernel Thread 직접적으로 매핑되지 않고, JVM 내에서 관리되는 경량화된 스레드로 OS에 의해 최대 생성 개수를 제한 받지 않고 JVM의 가용 Heap 메모리 내에서 제한 없이 Virtual Thread를 생성 가능합니다.
  • 블로킹 I/O 처리 시 Thread의 대기 상태를 개선
    • Thread는 블로킹 I/O 작업 시 대기 상태로 전환되고, 작업이 완료될 때까지 다른 작업을 처리할 수 없습니다.
    • Idle 상태의 Thread가 생깁니다.
    • Virtual Thread는 블로킹 I/O 작업 시 Carrier Thread에서 Unmount되고, 다른 Virtual Thread가 해당 Carrier Thread에 Mount되어 Carrier Thread는 Idle 상태가 되지 않고 지속적으로 동작하게 됩니다.


4. Continuation

Continuation은 Virtual Thread의 핵심 기술로, Virtual Thread 실행 상태를 저장하고, 필요할 때 다시 실행할 수 있도록 설계했습니다. 이를 통해 Virtual Thread가 블로킹 I/O 작업을 수행하면 Carrier Thread에서 Unmount되고 이때 다른 Virtual Thread Carrier Thread Mount되어 실행될 수 있습니다.

5. Coroutine(비동기 프로그래밍)과 비교 시 Virtual Thread의 장점

  • 제어 흐름 유지
    • 비동기 프로그래밍에서는 조건문이나 반복문과 같은 단순한 제어 흐름을 구현할 때 복잡한 코드 구조가 발생할 수 있습니다.
    • Callback 지옥과 같은 문제로 인해 부가적인 코드가 많아질 수 있습니다.
    • Virtual Thread를 사용하면 이러한 코드를 동기식으로 간단하게 작성 가능하게 해줍니다.
  • Context 유지
    • 비동기 프로그래밍은 요청이 여러 Thread를 넘나들며 처리되기 때문에 Context 정보가 스택 트레이스에 누적되지 않는 문제가 발생합니다.
    • 이로 인해 스택 트레이스가 쓸모가 없어집니다.
    • Virtual Thread는 Context를 유지하여 디버깅 및 문제 분석을 가능하게 해줍니다.
 

6. 성능 테스트(by Spring Boot v3.4.1, JDK 21)

Tomcat Thread Pool 설정

server:
  tomcat:
    threads:
      max: 50 # 스레드 최대 생성 개수
      min-spare: 5 # Idle 상태의 스레드 개수
      
spring:
  threads:
    virtual:
      enabled: true # Virtual Thread 활성화/비활성화

 

테스트 코드

@RequiredArgsConstructor
@RestController
public class ThreadController {

    @GetMapping("/test-threads")
    public String testThreads() throws InterruptedException {
        Thread.sleep(1000); // 1초 Sleep

        return "Hello Java~";
    }

}

 

Virtual Thread On/Off에 따른 테스트 결과

  • Threads: 41.8 TPS
  • Virtual Threads: 72.4 TPS
  • Virtual Threads를 사용할 경우, TPS가 약 73% 증가하여 성능이 크게 향상되는 것을 확인할 수 있었습니다.
    하지만, 테스트는 성능 차이를 내기위해 극한(?)의 상황을 만들어 놓은거라 요청이 적은 환경은 이보다 적은 성능 향상이 확인되었습니다.

7. 유의사항

  • ThreadLocal 사용 시 메모리 증가 주의
    • ThreadLocal은 Virtual Thread당 하나씩 생성되며
    • Virtual Thread는 수만개까지 생성이 될 수 있기 때문에 ThreadLocal 사용 시 의도치 않게 메모리 사용량이나 객체의 초기화 비용이 매우 커질 수 있다.
    • 방안: 컨텍스트 전파가 필요한 경우 Scoped Value 또는 ThreadLocalAccessor 사용합니다.
  • Virtual Thread Pinning Issue(JDK 24에서 개선 예정)
    • synchronized 블록에 진입하면 Virtual Thread가 Carrier Thread에서 Unmount 되지 못하는 상태(Pinning)가 발생하여 성능 저하가 발생합니다.
    • 방안 : ReentrantLock 등 java.util.concurrent 패키지의 락 구현체를 사용합니다.
  • 과도한 동시성으로 인한 리소스 부족 현상 발생 가능
    • DB Connection Pool 같은 제한된 자원에 동시 접근이 증가해 Timeout이 발생 가능합니다.
      (e.g: SQLTransientConnectionException)
    • 방안: Semaphore를 활용해 동시성을 제어하거나, Connection Pool 크기와 Virtual Thread 수를 적절히 조정하여 해결합니다.
  • CPU Bound 작업에는 비효율적
    • Virtual Thread는 Blocking 상황이 발생할 때, Platform Thread가 대기하는 상황을 줄이기 위한 패러다임입니다.
    • CPU Bound 작업은 Virtual Thread의 Mount/Unmount 과정이 오히려 불필요한 과정이 되어 Platform Thread보다 성능이 떨어집니다.
    • 적합한 환경: IO Bound 작업에 효율적입니다.
  • Structured Concurrency 활용
    • 기존 CompletableFuture 체인은 작업 실패 시 리소스 누수나 에러 전파가 불명확합니다.
    • 방안: StructuredTaskScope를 사용해 작업 그룹의 생명주기를 명시적으로 관리합니다.

 

 

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함