프로그래밍 공부/Java
JDK 21, Virtual Thread
로ᄏl
2024. 3. 17. 18:03
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