1. Virtual Thread?
(1) 도입 배경
- 기존 자바 스레드인 platform thread 의 한계
- platform thread : OS thread 를 래핑한 형태이며, 자바의 전통적인 스레드이다.
- (Java thread code 는 실제 OS thread 를 이용하는 방식으로 동작한다.)
- OS 커널에서 사용할 수 있는 스레드 갯수가 제한적이고 생성, 유지 비용이 비싸다.
- 이를 효율적으로 사용하기 위해 thread pool 사용
- 처리량(throughput) 한계
- Thread Per Request 으로 사용자 요청을 처리하지만 OS 스레드는 무한정 늘릴 수 없는 단점이다.
- application throughput 은 thread pool 에 의존적이다.
- Blocking 으로 인한 리소스 낭비
- Thread Per Request 를 처리하는 스레드는 I/O 작업 처리시 Blocking 한다.
- I/O 작업이 완료될 때까지 다른 요청을 처리하지 못하고 대기해야 한다.
- Blocking 방식의 자원 낭비를 줄일 필요성이 있었다.
- Reactive Programming 단점
- 스레드 자원을 Non-Blocking 방식으로 변경하면서 다른 요청을 처리할 수 있는 방식이다.
- Reactive Programming 을 구현하기 위한 추가 학습이 필요해 코드 작성와 이해 비용이 높다.
- 스레드 중심의 자바 플랫폼 디자인
- 스레드 호출은 thread local 을 사용해 데이터, 컨텍스트를 연결하도록 설계
- (+ Exception, Debugger, Profile(JFR) 모두 스레드 기반)
- Reactive Programming 은 사용자의 요청이 스레드를 오고 가면서, 컨텍스트 확인이 어려워 디버깅이 어렵다.
[해결하고자 했던 문제]
(1) I/O 작업을 처리시, 스레드 자원을 효율적으로 사용하면서 처리량(throughput) 을 증가시키기
(2) 자바 플랫폼의 디자인과 조화를 이루는 코드 작성이 가능하도록 하기
(2) Challenge (해결 방안) - Virtual Thread
- I/O 작업을 처리시, 스레드 자원을 효율적으로 사용하면서 처리량(throughput) 을 증가시키기
- Blocking 으로 인한 대기 문제
- 내부 스케줄링을 통해 플랫폼 스레드가 대기하지 않고 다른 가상 스레드가 작업한다.
- (Non-blocking 에 대한 처리를 JVM 레벨에서 담당)
- Blocking 으로 인한 대기 문제
- 자바 플랫폼의 디자인과 조화를 이루는 코드 작성이 가능하도록 하기
- 기존 스레드 구조를 그대로 사용하기 때문에 디버깅, 프로파일링등 기존의 도구도 그대로 사용
2. Platform, Virtual Thread 구조 차이
(1) Platform Thread

- 플랫폼 스레드는 실제 OS 스레드를 사용하는 것과 같음
- 비용이 비싸기 때문에 스레드 풀을 사용
(2) Virtual Thread

- carrier thread : virtual thread 와 OS thread 를 연결해주는 스레드
- mount/unmount : JVM 자체적으로 virtual thread 를 OS 연결하는 과정 (JVM 이 스케줄링)
- virtual thread 가 Blocking 이 발생하면 내부 스케줄링을 통해 Carrier Thread 를 다른 virtual thread 의 작업을 처리
3. 스프링 부트에서 사용하기
- JDK 21 은 2023.09.19 정식 릴리즈
- Gradle 버전은 8.4 버전 이상에서 JDK 21을 지원
- Spring Boot 3.2 가 2023.11.23 정식 릴리즈되어 JDK 21을 지원
(1) 적용방법
- spring boot version >= 3.2, spring.threads.virtual.enabled=true 프로퍼티 설정
- spring boot version < 3.2, virtual thread executor bean 등록
// 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());
}
(2) 성능 테스트 결론
- Thread Blocking 없는 경우 throughput
- Platform Thread > Virtual Thread
- (Virtual Thread Scheduling 을 위한 오버헤드로 예상)
- Thread Blocking 이 발생하는 경우 throughput
- Platform Thread < Virtual Thread
- DB Query 에서 Virtual Thread 사용시 SQLTransientConnectionException 발생
- Connection timeout(30s)이 발생.
- DB Connection pool 의 한정된 자원의 병목 지점으로 인한 문제
- → 한정된 자원 접근 제한을 위한 방법 고려 (e.g. semaphore)
(3) 주의사항
1. 개별 작업에 virtual thread 할당하는 형태로 변경하기
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(task1);
executor.submit(task2);
}
2. ThreadLocal 에 값비싼 객체 캐싱 금지
static final ThreadLocal<SimpleDateFormat> cachedFormatter =
ThreadLocal.withInitial(SimpleDateFormat::new);
...
cachedFormatter.get().format(...);
=====>>>>>>>
static final DateTimeFormatter formatter = DateTimeFormatter....;
...
formatter.format(...);
- 플랫폼 스레드의 비용이 비싸 여러 작업 사이에 공유하는 형태로 개발
- 가상 스레드는 작업당 하나를 활용할 것을 권장되며, 내부 객체를 공유하지 않는다.
- 가상 스레드가 예상보다 더 많은 메모리를 사용하는 원인이 될 수 있음.
3. synchronized keyword 주의
synchronized(lockObj) {
frequentIO();
}
=====>>>>
lock.lock();
try {
frequentIO();
} finally {
lock.unlock();
}
- pinning : blocking I/O 작업 수행하는 경우 virtual thread unmount 가 불가능해 Carrier Thread 까지 Blocking 되는 현상 발생 → virtual thread 장점 활용 못함
- synchronized가 필요한 경우 자바의 동시성 유틸리티에 있는 명시적인 lock 을 사용하자.
Reference
'java > summary' 카테고리의 다른 글
Java Local Cache 비교하기 (0) | 2025.03.09 |
---|---|
SerialVersionUID를 선언해야 하는 이유 (0) | 2024.11.28 |
자바 비동기로 카페 콘솔 예제 만들기 (1) | 2024.11.26 |
어댑터 패턴(adapter pattern) (1) | 2024.07.09 |
퍼사드 패턴(Facade Pattern) (0) | 2024.07.08 |
1. Virtual Thread?
(1) 도입 배경
- 기존 자바 스레드인 platform thread 의 한계
- platform thread : OS thread 를 래핑한 형태이며, 자바의 전통적인 스레드이다.
- (Java thread code 는 실제 OS thread 를 이용하는 방식으로 동작한다.)
- OS 커널에서 사용할 수 있는 스레드 갯수가 제한적이고 생성, 유지 비용이 비싸다.
- 이를 효율적으로 사용하기 위해 thread pool 사용
- 처리량(throughput) 한계
- Thread Per Request 으로 사용자 요청을 처리하지만 OS 스레드는 무한정 늘릴 수 없는 단점이다.
- application throughput 은 thread pool 에 의존적이다.
- Blocking 으로 인한 리소스 낭비
- Thread Per Request 를 처리하는 스레드는 I/O 작업 처리시 Blocking 한다.
- I/O 작업이 완료될 때까지 다른 요청을 처리하지 못하고 대기해야 한다.
- Blocking 방식의 자원 낭비를 줄일 필요성이 있었다.
- Reactive Programming 단점
- 스레드 자원을 Non-Blocking 방식으로 변경하면서 다른 요청을 처리할 수 있는 방식이다.
- Reactive Programming 을 구현하기 위한 추가 학습이 필요해 코드 작성와 이해 비용이 높다.
- 스레드 중심의 자바 플랫폼 디자인
- 스레드 호출은 thread local 을 사용해 데이터, 컨텍스트를 연결하도록 설계
- (+ Exception, Debugger, Profile(JFR) 모두 스레드 기반)
- Reactive Programming 은 사용자의 요청이 스레드를 오고 가면서, 컨텍스트 확인이 어려워 디버깅이 어렵다.
[해결하고자 했던 문제]
(1) I/O 작업을 처리시, 스레드 자원을 효율적으로 사용하면서 처리량(throughput) 을 증가시키기
(2) 자바 플랫폼의 디자인과 조화를 이루는 코드 작성이 가능하도록 하기
(2) Challenge (해결 방안) - Virtual Thread
- I/O 작업을 처리시, 스레드 자원을 효율적으로 사용하면서 처리량(throughput) 을 증가시키기
- Blocking 으로 인한 대기 문제
- 내부 스케줄링을 통해 플랫폼 스레드가 대기하지 않고 다른 가상 스레드가 작업한다.
- (Non-blocking 에 대한 처리를 JVM 레벨에서 담당)
- Blocking 으로 인한 대기 문제
- 자바 플랫폼의 디자인과 조화를 이루는 코드 작성이 가능하도록 하기
- 기존 스레드 구조를 그대로 사용하기 때문에 디버깅, 프로파일링등 기존의 도구도 그대로 사용
2. Platform, Virtual Thread 구조 차이
(1) Platform Thread

- 플랫폼 스레드는 실제 OS 스레드를 사용하는 것과 같음
- 비용이 비싸기 때문에 스레드 풀을 사용
(2) Virtual Thread

- carrier thread : virtual thread 와 OS thread 를 연결해주는 스레드
- mount/unmount : JVM 자체적으로 virtual thread 를 OS 연결하는 과정 (JVM 이 스케줄링)
- virtual thread 가 Blocking 이 발생하면 내부 스케줄링을 통해 Carrier Thread 를 다른 virtual thread 의 작업을 처리
3. 스프링 부트에서 사용하기
- JDK 21 은 2023.09.19 정식 릴리즈
- Gradle 버전은 8.4 버전 이상에서 JDK 21을 지원
- Spring Boot 3.2 가 2023.11.23 정식 릴리즈되어 JDK 21을 지원
(1) 적용방법
- spring boot version >= 3.2, spring.threads.virtual.enabled=true 프로퍼티 설정
- spring boot version < 3.2, virtual thread executor bean 등록
// 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());
}
(2) 성능 테스트 결론
- Thread Blocking 없는 경우 throughput
- Platform Thread > Virtual Thread
- (Virtual Thread Scheduling 을 위한 오버헤드로 예상)
- Thread Blocking 이 발생하는 경우 throughput
- Platform Thread < Virtual Thread
- DB Query 에서 Virtual Thread 사용시 SQLTransientConnectionException 발생
- Connection timeout(30s)이 발생.
- DB Connection pool 의 한정된 자원의 병목 지점으로 인한 문제
- → 한정된 자원 접근 제한을 위한 방법 고려 (e.g. semaphore)
(3) 주의사항
1. 개별 작업에 virtual thread 할당하는 형태로 변경하기
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(task1);
executor.submit(task2);
}
2. ThreadLocal 에 값비싼 객체 캐싱 금지
static final ThreadLocal<SimpleDateFormat> cachedFormatter =
ThreadLocal.withInitial(SimpleDateFormat::new);
...
cachedFormatter.get().format(...);
=====>>>>>>>
static final DateTimeFormatter formatter = DateTimeFormatter....;
...
formatter.format(...);
- 플랫폼 스레드의 비용이 비싸 여러 작업 사이에 공유하는 형태로 개발
- 가상 스레드는 작업당 하나를 활용할 것을 권장되며, 내부 객체를 공유하지 않는다.
- 가상 스레드가 예상보다 더 많은 메모리를 사용하는 원인이 될 수 있음.
3. synchronized keyword 주의
synchronized(lockObj) {
frequentIO();
}
=====>>>>
lock.lock();
try {
frequentIO();
} finally {
lock.unlock();
}
- pinning : blocking I/O 작업 수행하는 경우 virtual thread unmount 가 불가능해 Carrier Thread 까지 Blocking 되는 현상 발생 → virtual thread 장점 활용 못함
- synchronized가 필요한 경우 자바의 동시성 유틸리티에 있는 명시적인 lock 을 사용하자.
Reference
'java > summary' 카테고리의 다른 글
Java Local Cache 비교하기 (0) | 2025.03.09 |
---|---|
SerialVersionUID를 선언해야 하는 이유 (0) | 2024.11.28 |
자바 비동기로 카페 콘솔 예제 만들기 (1) | 2024.11.26 |
어댑터 패턴(adapter pattern) (1) | 2024.07.09 |
퍼사드 패턴(Facade Pattern) (0) | 2024.07.08 |