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
- https://openjdk.org/jeps/444
- https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-DC4306FC-D6C1-4BCC-AECE-48C32C1A8DAA
- https://medium.com/@phoenixrising_93140/issue-with-java-virtual-threads-e91d85eecbc9
- https://levelup.gitconnected.com/continuation-and-virtual-threads-in-java-a-new-concurrency-paradigm-8cac3903c46a
- https://tech.kakao.com/2023/12/22/techmeet-virtualthread/
- https://techblog.woowahan.com/15398/
- https://findstar.pe.kr/2023/04/17/java-virtual-threads-1/
- https://findstar.pe.kr/2023/07/02/java-virtual-threads-2/
- https://jaeyeong951.medium.com/virtual-thread-synchronized-x-6b19aaa09af1
728x90
반응형