본문 바로가기
카테고리 없음

[JAVA] Java Virtual Thread

728x90

 

 

 

 


가상스레드(Virtual Thread)란?

 

Def.

Java21에 추가된 java.lang.VirtualThread 클래스이다. OS 스레드에 1:1 매핑되는 것이 아닌 JVM에서 사용하는 경량 스레드 개념이다.

 

Goal

- 기존 서버 어플리케이션(1요청=1스레드)의 하드웨어 사용률을 높인다.

- VirtualThread는 Thread를 상속하고 있기 때문에 코드 수정을 최소화한다.

- easy Troubleshooting/Debugging/Profiling

 

 

 


Thread vs Virtual Thread

 Thread 

- OS에서 제공하는 커널 스레드를 1:1 wrapping해서 사용(Platform thread)
- 어플리케이션에서는 스레드풀을 생성해서 관리
- 하나의 웹 요청 = 하나의 스레드
- I/O 작업시에는 스레드가 blocking되며 작업처리시간보다 대기시간이 김

 

VirtualThread

- ForkJoinPool을 통해 CarrierThread(기존 PlatformThread) 관리
- CarrierThread들은 VirtualThread들의 task를 처리하다가 blocking 발생하면 다른 VirtualThread task 실행
- VirtualThread는 OS 자원 할당이 아닌 task별로 할당하는 개념

 

 

 


⬛ 동작 방식

 지원 버전

- Java 21
- gradle 8.4
- Spring Boot 3.2.0 정식지원
- IntelliJ 2023.03

 

VirtualThread

final class VirtualThread extends BaseVirtualThread {
    ...
 
    // scheduler and continuation
    private final Executor scheduler;   // ForkJoinPool: CarrierThread의 pool
    private final Continuation cont;    // 스레드 실행 상태의 snapshot. 스레드가 park되면 상태저장
    private final Runnable runContinuation; // task
 
    ...
}

1. runContinuation들 CarrieThread의 workQueue에 push
2. 작업중 blocking 발생 시 workQueue에서 pop, 상태를 힙 메모리에 저장
3. VirtualThread들은 별도 pooling하지 않음 → GC


 Flow


 어플리케이션 적용

Spring Boot 3.2.0~

spring:
  threads:
    virtual:
      enabled: true

 

Spring Boot 3.X

@Configuration
public class Configuration {
    // Async Task
    @Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
    public AsyncTaskExecutor asyncTaskExecutor() {
        return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
    }
 
    // Tomcat 요청 VirtualThread로 처리
    @Bean
    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
        return protocolHandler -> {
            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
        };
    }
}

 

 

 


⬛ Test

 Tomcat 처리

- Thread sleep 3초를 걸고 로그 확인

- 3번의 Request

 

Thread

server:
  tomcat:
    threads:
      max: 2
 
 
#spring:
#  threads:
#    virtual:
#      enabled: true
 
 
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping
    public void test() throws Exception {
        Thread th =  Thread.currentThread();
        log.info("[{}]request start", th);
        Thread.sleep(3000);
        log.info("[{}]request end", th);
    }
}

스레드를 2개로 제한했기 때문에 2개요청이 blocking되고 3번째 요청 처리가 지연된 것을 확인할 수있다.


Virtual Thread

server:
  tomcat:
    threads:
      max: 2
 
 
spring:
  threads:
    virtual:
      enabled: true
 
 
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping
    public void test() throws Exception {
        Thread th =  Thread.currentThread();
        log.info("[{}]request start", th);
        Thread.sleep(3000);
        log.info("[{}]request end", th);
    }
}

앞선 2개 요청이 blocking되어도 곧바로 3번째 요청 처리가 된 것을 확인할 수있다.

 

 

 ExecutorService

- Thread sleep 3초가 있는 task 20개 요청

- 10개의 스레드풀을 가진 executor와 10개의 ForkJoinPool을 가진 가상스레드 비교

 

Thread

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/test")
public class TestController {
 
    private final ExecutorService service = Executors.newFixedThreadPool(10);
 
    @GetMapping
    public void test(){
        Runnable task = () -> {
            try {
                log.info("Thread: {}",Thread.currentThread());
                Thread.sleep(3000);
                log.info("END Thread: {}", Thread.currentThread());
            } catch (Exception exception) {
 
            }
        };
 
        for (int i = 0; i < 20; i++) {
            service.submit(task);
        }
    }
}

앞선 10개의 task처리중 blocking 발생으로 후속 10개 task 지연 발생 확인

 

Virtual Thread

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/test")
public class TestController {
 
    private final ExecutorService service = Executors.newVirtualThreadPerTaskExecutor();
 
    @GetMapping
    public void test(){
        Runnable task = () -> {
            try {
                log.info("Thread: {}",Thread.currentThread());
                Thread.sleep(3000);
                log.info("END Thread: {}", Thread.currentThread());
            } catch (Exception exception) {
 
            }
        };
 
        for (int i = 0; i < 20; i++) {
            service.submit(task);
        }
    }
}

앞선 10개 task에 blocking이 발생해도 곧바로 다음 task를 수행하는 것을 확인

 

 

 


⬛ 주의사항

  • VirtualThead는 동시 작업수가 많으면서 CPU-bound 작업이 아닐때 throughput을 향상시킨다. CPU 작업시에는 오히려 성능저하(기존스레드 + VirtualThread, 스케줄링 등 추가비용)
  • Pinning issue: synchronized 키워드, JNI native call하게 되면 해당 VirtualThread와 연결된 CarrierThread가 blocking된다. (기존 스레드와 같이)                     → 대체 방안으로 ReentrantLock 사용 권장, JVM 옵션으로 모니터링 가능 -Djdk.tracePinnedThreads=full or -Djdk.tracePinnedThreads=short
  • ThreadLocal 사용시 주의. VirtualThread는 무수히 많이 사용할 수 있기 때문에 메모리 이슈 가능성 있음. 
  • Tomcat에서 throughput을 빠르게 소화하더라도, JDBC connection 처리에서 밀릴 수 있음. (MySQL JDBC 내부적으로 synchronized 블록을 많이 사용하고 있어 VirtualThread와 궁합이 맞지 않는다는 의견)

 

 

 


📝 요약

◾ Java21에 Virtual Thread가 추가됨. os 스레드 매핑이 아닌 JVM에서 사용하는 경량 스레드

◾ I/O 같은 blocking 작업이 많을 시 다른 작업을 할 수 있어 하드웨어 사용률을 높인다.

◾ CPU- bound 작업시에는 성능저하 우려

 

 

 


📃 Ref

 

728x90
반응형