JDK 21, Virtual Thread

728x90

Virtual Thread란…?

  • JDK 21에 새롭게 들어온 개념 (2023.09.19 에 LTS 출시)
    • gradle 8.4v 부터 지원
    • kotlin v1.9.20 부터 21 바이트 코드 지원
    • Spring 6.1, Spring boot 3.2 부터 지원
    • Jetbrain Intellij 2023.3

JDK 21(LTS)에 추가된 경량 스레드, OS 스레드를 그대로 사용하지 않고 JVM 내부 스케줄링을 통해서 수십만 ~ 수백만개의 스레드를 동시에 사용할 수 있게한다.

전통적인 Java의 Thread

  • Java의 Thread는 OS Thread를 랩핑한 것 (Platform Thread)
  • Java 애플리케이션에서 Thread를 사용하면 실제로 OS Thread를 사용한 것
  • OS Thread는 생성 갯수가 제한적이고 생성, 유지하는 비용이 비쌈
  • 이 때문에 애플리케이션에서는 플랫폼 스레드를 효율적으로 사용하기 위해 Thread Pool을 사용함
 

위와 같은 동작 매커니즘에의한 Throughput(처리량) 의 한계

  • 기본적인 Web Request 처리 방식은 Thread Per Request(하나의 요청/하나의 스레드)
  • 처리량을 높이려면 스레드 증가 필요, But 스레드는 한정적이다. (OS 스레드 제약)

특히나 Blocking I/O 쪽에서 많은 문제가 발생함.

  • Thread 에서 I/O 작업을 처리할 때, Blocking이 일어난다.
  • 작업을 처리하는 시간보다 대기하는 시간이 길다.
 

그래서 우리는 Reactive Programming을 사용했었음.

  • Webflux 스레드를 대기하지 않고 다른 작업 처리 가능 (장점)
  • 코드를 작성하고 이해하는 비용이 높다 (단점)
  • Reactive하게 동작하는 라이브러리 지원을 필요로 한다 (단점)
  • JPA를 사용할 수 없고 R2DBC라는 래퍼런스가 상대적으로 적은 라이브러리를 사용해야 함 (단점)
 

그렇다면 왜 이렇게 구성이 되어있을까…?

Java Design

  • 자바의 디자인은 ‘스레드 중심’으로 구성되어있다.
  • Exception Stack trace, Debugger, Profiling 모두 스레드 기반
  • Reactive할 때, 작업이 여러 스레드를 거쳐 처리되는데, 컨택스트 확인이 어려워 디버깅이 어려움.

Virtual Thread가 해결하고자하는 문제

  • 애플리케이션의 높은 처리량 확보
    • Blocking 발생시 내부 스케줄링을 통해 다른 작업을 처리 (= 기존 자바의 한계)
  • 자바 플랫폼의 디자인과 조화를 이루는 코드 생성
    • 기존 스레드 구조 그대로 사용(= Webflux가 못한부분)
 

결론은 Virtual Thread는 Reacitve와 MVC의 장점만 차용한 케이스!

 
 

Virtual Thread가 앞에 따로 존재함.
뒤에 Fork/Join Pool이 Carrier Thread(Platform Thread와 거의 동일한 형태)
Carrier Thread는 OS Thread와 1:1 매핑되는 구조이긴 하지만 실제 Application에서는 Platform Thread가 아니라 Virtual Thread만 사용하게된다.

 
  • Virtual Thread가 Blocking되면 Virtual Thread와 Carrier Thread에서 Unmount 된다.
  • 그리고 다른 Virtual Thread가 해당 Carrier Thread와 Mount 된다.
  • OS Thread가 갯수에 제한이 있는 것에 비해서 Virtual Thread는 엄청난게 많이 생성할 수 있다.
 

사용법 예시

// Virtual Thread 방법 1
Thread.startVirtualThread(() -> {
    System.out.println("Hello Virtual Thread");
});

// Virtual Thread 방법 2
Runnable runnable = () -> System.out.println("Hi Virtual Thread");
Thread virtualThread1 = Thread.ofVirutal().start(runnable);

// Virtual Thread 이름 지정 
Thread.Builder builder = Thread.ofVirtual().name("JVM-Thread");
Thread virtualThread2 = builder.start(runnable);

// 스레드가 Virtual Thread인지 확인하여 출력
System.out.println("Thread is Virtual? " + virtualThread2.isVirtual());

// ExecutorService 사용 
try(final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 3; i++) {
        executorService.submit(runnable);
    }
}

Spring Boot(MVC) 적용법 (3.2 이상)

# application.yaml

spring:
  threads:
    virtual:
      enabled: true

Spring Boot(MVC) 적용법 (3.x)

// Web Request를 처리하는 Tomcat이 Virtual Thread를 사용하여 유입된 요청을 처리하도록 한다. 
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
    return protocolHandler -> {
        protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
    }
}

// Async Task에 Virtual Thread 사용
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
public AsyncTaskExecutor asyncTaskExecutor() {
    return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}

유의사항

유의사항 1

  • Platform Thread => Virtual Thread (X)
    • 전통적으로 사용하던 이 개념으로 Virtual Thread를 쓰게되면 성능 향상을 체감할 수 없을 것이다.
  • Task => Virtual Thread (O)

리소스라고 생각하기보다는 Task별로 Virtual Thread 할당

유의사항 2

Thread Local 사용시 주의

  • Platform Thread Pool을 사용할 때, 공유를 위해 ThreadLocal을 사용하던 관습
  • Virtual Thread는 Heap을 사용하기 때문에 이를 남발하면 메모리 사용이 늘어남.
    • Platform Thread와는 달리 Virtual Thread는 수십 수백만개까지 생성될 수 있기 떄문에 메모리 점유량이 확 늘어날 수 있어 메모리 이슈 발생 가능성이 있음.

유의사항 3

synchronized 사용시 주의

synchronized 사용시 Virtual Thread에 연결된 Carrier Thread가 Blocking 될 수 있으니 주의
(이런 경우를 pinning 이라고 함)

// synchronized 사용 (pinning 발생)
// 순차적 접근을 보장한다. 
public synchronized String accessResource() {
    return access();
}

// ReentrantLock 사용 (pinning 발생하지 않음)
private static final ReentrantLock LOCK = new ReentrantLock();

public String accessResource() {
    // 순차적 접근을 보장한다.
    LOCK.lock();
    try {
        return access();
    } finally {
        LOCK.unlock();
    }
}

I/O와 관련된 부분에 있어서는 Virtual Thread를 적용했을 때, 상당히 큰 효과를 누릴 수 있다.

적합한 사용처

  • I/O Blocking이 발생하는 경우 Virtual Thread가 적합
  • CPU Intensive 작업에는 적합하지 않음
    • ex) 이미지 프로세싱을해서 썸네일을 만든다. 동영상 인코딩을 한다거나. (I/O Intensive 작업이 아니라 CPU Intensive 작업이기 때문에 Virutal Thread를 도입한다고해서 크게 달라질게 없다)
  • Spring MVC 기반 Web API 제공시 편리하게 사용할 수 있다.
    • 높은 Throughput을 위해서 Webflux를 고려중이라면 대안이 될 수 있다.
    • Webflux 도입에도 (1)Stream 형태의 서비스를 제공해야하는 경우 vs (2)단순히 많은 처리를 하고 싶어서 가 있을 수 있는데, 이 중 (2)의 케이스는 Virtual Thread라는 새로운 대안으로 고민해볼 수 있다.

Virtual Thread에 대한 오해

  • Virtual Thread는 기존(Platform) Thread를 대체하는 것이 목적이 아니다.
  • Virtual Thread는 기다림에 대한 개선, 그리고 플랫폼 디자인과의 조화
  • 도입한다고 무조건 처리량이 높아지지 않는다.
  • Virtual Thread는 그 자체로 Java의 동시성을 완전히 개선했다고 보기는 어렵다.

Virtual Thread의 제약

  • Thread Pool에 적합하지 않다. Task 별로 Virtual Thread를 할당해야 함
  • Thread Local 사용시 메모리 사용이 늘어날 수 있음
  • synchronized 사용시 주의가 필요함. (carrier thread가 blocking 될 수 있음)
    • ReentrantLock을 사용
  • 제한된 리소스의 경우 semaphore를 사용

참고 자료

  • https://www.youtube.com/watch?v=vQP6Rs-ywlQ
  • https://techblog.woowahan.com/15398/
728x90