Java 가상 스레드 (Virtual Thread)

U_Uracil·2024년 1월 9일
3

Java

목록 보기
2/2

Virtual Thread

  • JDK 19 Preview, JDK 21 Release
  • 처리량이 높고
  • 유지 보수 및 디버깅이 수월해지는
  • 경량 스레드

탄생 배경


0. Thread의 수명 주기

1. 기존 Java의 스레드 모델

  • 기존 Java의 스레드는 OS 스레드를 Wrapping한 Platform 스레드를 사용
  • Java 애플리케이션에서 스레드를 사용하면 실제로는 OS 스레드를 사용한 것
  • OS 스레드는 갯수가 제한적이고 생성 및 유지 비용이 비싸다는 단점 있음
  • 그렇기에 스레드 풀(Thread Pool)을 만들어 사용해옴

2. 처리량(throughput)의 한계

  • Spring Boot와 같은 애플리케이션의 기본적인 사용자 요청 처리 방식은 Thread Per Request
  • 즉, 하나의 request를 처리하기 위해 하나의 thread를 사용
  • 처리량을 늘리려면 스레드 수를 늘려야 하지만 OS 스레드는 쉽게 늘릴 수 없음
  • 그러므로 애플리케이션의 처리량스레드 풀에서 감당할 수 있는 범위까지만 가능

3. Blocking으로 인한 리소스 낭비

  • 기존 Thread Per Request 모델은 요청 처리 스레드에서 I/O 작업 시 Blocking이 일어남
  • I/O 작업을 마칠 때까지 해당 스레드는 다른 요청을 처리하지 못하고 대기
  • 이런 문제 때문에 Non-blocking 방식의 Reactive Programming 발전

4. Reactive Programming의 단점

  • 비동기 방식의 Reactive Programming
  • Blocking 방식을 Non-blocking 방식으로 변경
  • 대기 시간을 줄였지만, 가파른 러닝 커브 비용 발생
  • 기존 Java 프로그래밍은 스레드 기반이므로 라이브러리를 Reactive 방식에 맞게 새롭게 작성해야 하는 문제 발생

Virtual Thread는 위의 문제를 해결하기 위해 Project Loom의 결과로 탄생


Virtual Thread의 목표


JEP 444에서 밝힌 Virtual Thread의 목표는 아래와 같다.

  • Enable server applications written in the simple thread-per-request style to scale with near-optimal hardware utilization.
  • Enable existing code that uses the java.lang.Thread API to adopt virtual threads with minimal change.
  • Enable easy troubleshooting, debugging, and profiling of virtual threads with existing JDK tools.

위의 목표를 어떻게 달성하는지 하나씩 알아보자

Virtual Thread 구조


  • 기존 방식과 비교하면 JVM에서 Scheduling 과정이 추가됨
  • 기존 thread-per-request 방식에서는 Blocking 발생 시 대기
  • Virtual Thread는 JVM에서의 Scheduling을 통해 다른 Virtual Thread의 작업을 처리

장점

  • Blocking으로 인한 대기 시간이 없어 리소스를 효율적으로 사용하여 높은 처리량을 감당할 수 있음
  • Reactive Programming과 다르게 Non-blocking 처리를 JVM 레벨에서 담당
  • 자원 사용량(메모리, 컨텍스트 스위치 비용)이 플랫폼 스레드에 비해 적음
    • 플랫폼 스레드와 가상 스레드 비교

      플랫폼 스레드가상 스레드
      메타 데이터 사이즈약 2kb(OS별 차이 존재)200~300 B
      메모리미리 할당된 Stack 사용필요할 때 Heap 사용
      컨텍스트 스위칭 비용1~10usns (OR 1us 미만)
      생성 시간~1ms~10us

동작 원리


  1. 실행될 virtual thread의 작업인 runContinuation을 carrier thread의 work queue에 추가(push)
  2. Work queue에 있는 runContinuation들이 forkJoinPool에 의해 work stealing 방식으로 carrier thread에서 처리
  3. 처리되던 runContinuation들은 I/O, Sleep으로 인한 interrupt나 작업 완료 시 work queue에서 pop되어 힙 메모리로 돌아감

Work Stealing과 Fork-Join

  • Work Stealing 알고리즘
    • 병렬 처리를 위한 알고리즘으로 일정한 개수의 스레드를 유지하고, 스레드마다 독립적인 작업 큐를 관리하여 하나의 스레드 큐가 비게 되면 다른 스레드에서 task를 훔쳐오는 방식
  • Fork-Join 프레임워크
    • Work Stealing 알고리즘을 구현한 Java 7부터 추가된 프레임워크
    • 작업을 잘게 나눌 수 있을 때까지 split하고, 작업 큐에 있는 tail task를 다른 쓰레드가 나누어 병렬처리 한 후 join하여 합산

생성 및 사용


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(() -> {
	        System.out.println("Hello Virtual Thread");
         });
         
         // Virtual Thread 방법 2
         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);
            }
         }
    }
}
  • 기존의 스레드(플랫폼 스레드)를 생성하는 문법과 큰 차이가 없이 가상 스레드를 생성할 수 있음
    • 가상 스레드는 특정 OS 스레드에 연결되지 않은 java.lang.Thread 의 인스턴스로, 기존 스레드 사용 코드를 조금만 변경해도 사용 가능함
    • 기존 Java 플랫폼의 멀티스레드 설계 및 툴링과 조화롭게 유지 가능

Virtual Thread 사용 시 주의 사항


[Pooling 금지]

  • 풀링은 고가의 리소스를 공유하기 위한 목적
  • 가상 스레드는 라이프사이클 동안 하나의 작업만 실행하도록 설계되었음
  • 필요할 때마다 생성하고 사용 후에는 GC에 의해 소멸되도록 하는 것이 좋음
  • 동시 요청의 수를 제한하기 위해 스레드 풀을 사용하는 코드는 풀링 대신 세마포어 등을 사용하도록 수정

[CPU bound 작업에서 비효율적]

  • I/O 작업 없이 CPU 작업만 수행하는 경우, 오히려 플랫폼 스레드보다 성능이 떨어짐
  • 가상 스레드는 JVM에서의 컨텍스트 스위칭 오버헤드가 존재하기 때문

[Thread Local의 사용]

  • Thread Local이란?
    • 여러 개의 스레드가 존재할 때, 해당 스레드만 접근 가능한 특별한 저장소
    • 현재 스레드의 실행과 연관된 데이터를 다루는 기법
    • 멀티 스레드 환경에서 공유 자원의 동시성 문제 등을 해결
  • 스레드 로컬은 여러 문제를 갖고 있고, 이로 인한 메모리 누수 혹은 메모리 에러 발생 가능성 있음
  • 가상 스레드는 최대한 가볍게 이용하는 것이 목적이므로 무겁고 큰 스레드 로컬을 사용하지 않는 것이 좋음

[Pinned issue]

  • 가상 스레드 내에서 synchronized, parallelStream, 혹은 네이티브 메서드를 사용하면 가상 스레드가 캐리어 스레드에 고정되는 문제가 있음
  • 이런 경우, 가상 스레드의 장점인 Non-blocking 방식으로 동작하지 않음
  • synchronized 대신 ReentrantLock을 사용하는 것이 좋음

사용할만한 곳

  • I/O Blocking이 발생하는 경우 (CPU 종속 작업 X)
  • Spring MVC 기반 Web API 제공 시 높은 처리량을 위해 Webflux를 고려중일 때 대안으로 사용


References
https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-A0E4C745-6BC3-4DAE-87ED-E4A094D20A38
https://openjdk.org/jeps/444
https://techblog.woowahan.com/15398/
https://mangkyu.tistory.com/309
https://mangkyu.tistory.com/317
https://jaeyeong951.medium.com/virtual-thread-synchronized-x-6b19aaa09af1
https://medium.com/deno-the-complete-reference/springboot-virtual-threads-vs-webflux-performance-comparison-for-jwt-verify-and-mysql-query-ff94cf251c2c
****https://www.youtube.com/watch?v=Q1jZtN8oMnU&ab_channel=우아한테크
https://findstar.pe.kr/2023/07/02/java-virtual-threads-2/
https://velog.io/@vies00/Java-work-stealing-fork-join-xljtjnflly

profile
기억은 유한, 기록은 무한

0개의 댓글