ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java 21. 가상 스레드
    BackEnd/Java 2024. 12. 15. 07:00
    반응형
    • 프로세스(Process): 컴퓨터에서 실행되고 있는 프로그램입니다.
    • 스레드(Thread): 프로세스 내 더 가벼운 공유 메모리 단위로, 작업의 효율적인 병렬 실행을 가능하게 하는 실행 흐름의 단위입니다.

     

    Native Thread Model

      Java에서 스레드를 만들어 실행하게 되면, JVM 내부의 자바 스레드 객체와 OS의 커널 스레드(Native Thread)가 모두 생성됩니다. 자바 스레드와 OS의 커널 스레드가 1:1로 매핑되기 때문에 1:1 스레딩 모델이라고도 부릅니다. 다음 코드는 3개의 자바 스레드 객체와 3개의 OS 커널 스레드를 생성하고 1:1로 매핑합니다.

      public static void threadExample01() throws Exception {
        for (int i = 0; i < 3; i++) {
          int threadNum = i + 1;
          Thread t = new Thread() {
            @Override
            public void run() {
              printlnWithThread(String.format("스레드 %s번 실행", threadNum));
            }
          };
          t.start();
        }
        
        // 우리가 만든 3개의 스레드가 실행될 때까지 기다리기 위해 5초간 대기한다!
        Thread.sleep(5_000L);
      }
      
      private static void printlnWithThread(Object obj) {
        System.out.printf("[%s] %s\n", Thread.currentThread().getName(), obj);
      }

     

      스레드를 사용하면 H/W 자원을 효율적으로 사용할 수 있습니다. 프로세스는 독립된 메모리를 가지고 있지만, 스레드는 Heap 메모리를 공유하고 Stack만 교체되기에 프로세스보다 비용이 적습니다. 하지만 스레드 또한 스레드 자체를 만들기 위해 비용이 든다는 단점이 있습니다. 이를 해결하기 위해 풀링 방식(스레드풀)을 사용합니다.

     

    스레드풀을 사용한 병렬 프로그래밍

      public static void threadExample02() throws Exception {
        try (ExecutorService executorService = Executors.newFixedThreadPool(2)) {
          for (int i = 0; i < 3; i++) {
            int threadNum = i + 1;
            executorService.submit(() -> printlnWithThread(String.format("스레드 %s번 실행", threadNum)));
          }
        }
        
        // 스레드 풀에 제출된 코드가 모두 실행될 때까지 기다리기 위해 5초간 대기한다!
        Thread.sleep(5_000L);
      }

     

    가상 스레드

      Java 21에서 도입된 경량 스레드로, 높은 동시성과 처리량을 제공하기 위해 설계되었습니다. 플랫폼 스레드(기존 스레드)는 네이티브 스레드와 1:1 매핑이었으나, 가상 스레드는 2개 이상의 가상 스레드를 만들어도 하나의 네이티브 스레드에 매핑될 수 있습니다. 실질적으로는 여러 가상 스레드가 하나의 플랫폼 스레드와 매핑되고, 플랫폼 스레드는 네이티브 스레드와 1:1 매핑되는 구조입니다. 이렇게 할당된 플랫폼 스레드를 캐리어(Carrier) 스레드라고 합니다.

    • mount: 가상 스레드가 플랫폼 스레드에 매핑되는 과정
    • unmount: 가상 스레드가 플랫폼 스레드에 해제되는 과정

     

    가상 스레드 특징

    • 경량성: 가상 스레드는 플랫폼 스레드(기존 스레드)보다 훨씬 적은 메모리를 사용하며, 생성 비용이 매우 낮습니다.
    • 대규모 동시성: JVM Heap 메모리가 허용하는 한 수백만 개의 가상 스레드를 생성할 수 있습니다.
    • 효율적인 I/O 처리: 블로킹 I/O 작업 시 가상 스레드는 일시 중단되고, 캐리어 스레드는 다른 가상 스레드를 실행할 수 있습니다.
    • 호환성: 기존 Java 스레드 모델과 완전히 호환되어 코드 변경 없이 사용할 수 있습니다.
    • 내부 스케줄링: JVM 내부에서 ForkJoinPool을 사용하여 스케줄링되므로, 시스템 콜 오버헤드가 감소합니다.
    • Context Switching 비용 감소: 가상 스레드 간 전환은 OS 수준의 Context Switching보다 훨씬 저렴합니다.
      public static void main(String[] args) throws Exception {
        // Thread.Builder
        Thread t = Thread.ofVirtual()
            .start(() -> printlnWithThread("Hello, Virtual Thread"));
        t.join();
        
        Thread t2 = Thread.startVirtualThread(() -> System.out.println("ABC"));
        
        // ExecutorService
        try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
          Future<?> future = myExecutor.submit(() -> printlnWithThread("Hello, Virtual Thread"));
          future.get();
        }
      }

     

    가상 스레드에 대한 오해

    • 가상 스레드가 플랫폼 스레드보다 코드를 더 빠르게 실행시키지는 않습니다.
    • 가상 스레드는 여러 요청을 동시에 처리해 전체 처리량을 늘리는 것이지, 단일 요청을 놓고 보면 플랫폼 스레드보다 느릴 수 있습니다.
    • 가상 스레드는 생성 비용이 플랫폼 스레드보다 저렴하기에 풀링 방식이 권장되지 않습니다. 즉, 스레드풀 사용이 권장되지 않습니다.

     

    가상 스레드의 등장 목적

    • 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 virtural threads with existing JDK tools.

     

    가상 스레드 + 스프링 MVC

      Spring Boot 3.2, Java 21부터 가상 스레드를 지원합니다. 다음 옵션으로 적용 가능하며, 옵션 적용 시 단순히 요청을 받는 톰캣 뿐만 아니라 스케줄러, @Async 처리, RabbitMQ / Kafka 리스너 등에서도 가상 스레드가 동작하게 됩니다.

    spring:
      threads:
        virtual:
          enabled: true

     

    주의점

    ThreadLocal

      가상 스레드는 매우 많이 생성될 수 있으므로, ThreadLocal에 무거운 객체를 저장하지 않아야 합니다. 사용 후에는 반드시 remove()로 정리하여 메모리 누수를 방지해야 합니다.

     

    DBCP(Database Connection Pool)

      풀링을 사용하지 않기 때문에 DBCP(Database Connection Pool)가 소진되어 요청이 거절될 수 있습니다. 플랫폼 스레드를 사용할 때는 스레드풀 개수만큼만 처리가 가능하여 데이터베이스에는 적당한 TPS가 들어가는 형태였습니다. 그러나 가상스레드의 경우 풀링을 사용하지 않고, 요청이 들어올 때마다 가상 스레드가 만들어지기 때문에 커넥션 풀이 전부 소진될 수 있습니다.

     

    Pinning 현상

      가상 스레드의 Pinning 현상이란 가상 스레드가 특정 플랫폼 스레드에 고정되어 버려 다른 가상 스레드를 실행시킬 수 없는 현상입니다. 해당 현상은 synchronized 블록 사용이나 네이티브 메소드, foreign 함수 실행 시 발생할 수 있습니다.

     

    가상 스레드 + Spring Webflux

      Spring Webflux는 적은 수의 스레드와 함께 non-blocking API로 동시성을 극대화하는 프레임워크로 적은 수의 스레드가 풀링되고 있어 가상 스레드를 바로 적용하기에는 애매한 상황입니다. 가상 스레드는 주로 블로킹 I/O 작업을 비동기적으로 처리하는 데 효과적인 반면, Webflux는 이미 non-blocking I/O를 사용하고 있기 때문입니다. 따라서 Spring Webflux에 가상 스레드를 적용하는 것은 즉각적인 성능 향상을 가져오지 않을 수 있으며, 오히려 기존의 최적화된 모델과 충돌할 가능성이 있습니다.

    반응형

    'BackEnd > Java' 카테고리의 다른 글

    Java 18-21. 주요 변경 내용  (0) 2024.12.13
    Java 21. Switch Pattern Matching  (0) 2024.12.12
    Java 21. Record Pattern  (0) 2024.12.11
    Java 12-17. 주요 변경 내용  (0) 2024.12.10
    Java 17. Sealed Class  (1) 2024.12.09

    댓글

Designed by Tistory.