2023.09.19 JDK 21 LTS
2023.10.04 Gradle 8.4
2023.11.17 Spring 6.1
2023.11.23 Spring boot 3.2
2023.12.07 Jetbrain Intellij
프로세스(Process) :
메모리에 올라와 실행되고 있는 프로그램의 인스턴스, 운영체제로부터 시스템 자원을 할당받는 작업의 단위
스레드(Thread) :
프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위
기존의 스레드를 그대로 상속하고 있다.
⇒ 디버깅, 프로파일링등 기존의 도구도 그대로 사용할 수 있도록 한다.
[ 가상 스레드 ]
Spring Web MVC 스타일로 코드를 작성하더라도 내부에서 가상 스레드가 기존의 플랫폼 스레드를 직접 사용하는 방식보다 Reactive programming 처럼 효율적으로 스케줄링하여 처리량을 높일 수 있다
🩷 Project Loom ?
경량의 스레드를 Java에 추가하기 위해서 가상 스레드를 비롯한 여러가지 기능들을 개발하는 프로젝트
5년동안 Virtual Thread를 개발하기 위해 노력을 했다고 함
기존 스레드의 사용성을 해치지 않면서도 가상 스레드를 내놓았다는 점에서 좋게 평가함
Virtual Thread 의 경우 Carrier Thread (Platform Thread 와 유사, 가상 스레드를 실제 OS와 연결해줌) 앞에 Virtual Thread 들이 따로 존재한다.
Carrier Thread 와 1:1 매핑되는 구조이지만 , 애플리케이션은 Thread Pool 없이 사용하고, JVM 자체적으로 가상 스레드를 OS 스레드와 연결하는 스케줄링한다.
가장 큰 차이점은 자체적인 Virtual Thread Scheduling
기존의 Thread는 Blocking 발생시 Carrier Thread(Platform Thread)가 기다리기만 했었다.
하지만, Blocking 발생시 버추얼 스레드는 Unmount(연결을 끊고) → 다른 버추얼 스레드가 해당 캐리어 스레드와 Mount(연결)을 한다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) throws Exception {
run();
}
public static void run() throws Exception {
// Virtual Thread 방법 1 : Thread.startVirtualThread() 빌더 사용
Thread.startVirtualThread(() -> {
System.out.println("Hello Virtual Thread");
});
// Virtual Thread 방법 2 : Thread.ofVirtual() 빌더 사용
Runnable runnable = () -> System.out.println("Hi Virtual Thread");
Thread virtualThread1 = Thread.ofVirtual().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 (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i <3; i++) {
executorService.submit(runnable);
}
}
}
}
# application.yml
spring:
threads:
virtual:
enabled: true
내부에서 발생하는 Tomcat, WAS 에 대한 처리를 Virtual Thread에서 감당하도록 처리해준다.
Platform Thread | Virtual Thread | |
---|---|---|
Stack size | ~ 2MB (OS 별 차이 있음) | ~10KB |
생성시간 | ~ 1ms | ~ 1µs |
Context Swiching cost | ~100 µs | ~10 µs |
Thread는 기본적으로 최대 2MB의 스택 메모리 사이즈를 가지기 때문에, 컨텍스트 스위칭 시 메모리 이동량이 크다. 또, 생성을 위해선 커널과 통신하여 스케줄링해야 하므로, 시스템 콜을 이용하기 때문에 생성 비용도 적지 않습니다.
하지만 Virtual Thread는 JVM에 의해 생성되기 때문에 시스템 콜과 같은 커널 영역의 호출이 적고, 메모리 크기가 일반 스레드의 1%에 불과합니다. 따라서 Thread에 비해 컨텍스트 스위칭 비용이 적다.
public String ioBound() {
requestSleep().block(); //Thread.sleep(300) API 호출
requestSleep().block();
requestSleep().block();
return "ok";
}
public Integer cpuBound() {
IntStream.range(0, 300000000).reduce(0, Integer::sum);
IntStream.range(0, 300000000).reduce(0, Integer::sum);
return IntStream.range(0, 300000000).reduce(0, Integer::sum);
}
사전 조건 : 테스트하기 위해 애플리케이션 스펙을 최소 사양으로 두고, 256MB의 힙 사이즈를 사용하도록 설정
테스트는 300ms를 sleep하는 API를 3번 호출하는 Request I/O Bound 작업, 0~300000000(3억)까지 합을 3번 계산하는 CPU Bound 작업으로 진행
I/O Bound 작업→ Virtual Thread의 성능은 Thread 모델에 비해 약 51% 이상 향상되었다
CPU Bound 작업 → 일반 스레드 모델이 성능상 우위를 보였다. Virtual Thread가 Switching 되지 않는 경우에는 Platform Thread 사용 비용뿐만 아니라 Virtual Thread 생성 및 스케줄링 비용까지 포함되어 성능 낭비가 발생되기 때문
Platform Thread → Virtual Thread (x)
Task → Virtual Thread (0)
개별 Task 를 Virtual Thread로 할당하는 것에 이점이 있다.
Thread Local 사용한다면 메모리 사용 증가를 주의 해야한다.
Virtual Thread는 수시로 생성되고, 소멸되며 스위칭이 된다.
Platform Thread Pool을 사용할때 공유를 위해 ThreadLocal을 사용하던 관습 (X)
Virtual Thread는 수십~수백만개까지 늘어날 수 있기에 내부에서 ThreadLocal을 남발해서 사용하면 메모리를 점유하게 되어 메모리 사용이 늘어난다.
Pinning issue
synchronized 사용시 Virtual Thread에 연결된 Carrier Thread가 Blocking이 될 수 있다
Virtual thread 내에서 synchronized 나 parallelStream 혹은 네이티브 메서드를 쓰면 Virtual Thread가 Carrier Thread에 고정되어 언마운트 할 수 없게 되는 문제 발생한다.
⇒ Spring은 synchronized를 ReentrantLock으로 마이그레이션 하는 방향
Virtual Thread 는 기존의 Platform Thread를 대체하는것이 목적이 아니다.
도입을 한다고 해서 무조건 처리량이 높아지지 않는다. 필요에 따라 적절히 사용해야 한다
- I/O Blocking 이 발생하는 경우 Virtual Thread 더 좋은 처리량을 보여준다
- CPU Bound 작업에는 적합하지 않다.
Spring MVC 기반 Web API 제공시 편리하게 사용할 수 있다
Virtual Thread는 기다림에 대한 개선, 그리고 플랫폼 디자인과의 조화
좋은 글 잘 읽고 갑니다 !