[자바] 가상 스레드

자바 19에서 프리뷰 기능으로 도입된 프로젝트 룸(Project Loom)의 가상 스레드(virtual thread)링크는 경량(lightweight) 스레드이다. 자바 20을 거쳐 자바 21에서 최종 릴리스링크되었다. 가상 스레드는 처리량이 많은 동시성 애플리케이션의 개발, 관리 및 관찰(observe)을 훨씬 용이하게 만든다. 요청 당 스레드를 할당하여 처리하는 서버 애플리케이션의 하드웨어 리소스 활용을 최적화하는 것이 가상 스레드 도입의 주 목적이다.

가상 스레드는 java.lang.Thread API를 사용하는 기존 코드가 최소한의 변경으로 가상 스레드를 사용할 수 있도록 지원한다. 또한 기존 JDK 도구를 사용하여 가상 스레드의 트러블슈팅, 디버깅 및 프로파일링을 쉽게 수행할 수 있다.


가상 스레드의 도입으로 변경되거나 대체되지 않는 것은 다음과 같다.

  • 애플리케이션의 기존 스레드 구현을 제거하지 않는다. 또한 기존 애플리케이션이 가상 스레드를 사용하도록 내부적으로 마이그레이션하지 않는다.
  • 자바의 기본적인 동시성 모델을 변경하지 않는다.
  • 자바 언어나 라이브러리에 새로운 데이터 병렬 처리 구조를 제공하지 않는다. 스트림 API는 여전히 대규모 데이터 세트를 병렬로 처리하는데 선호되는 방법이다.


가상 스레드의 도입 배경

일반적으로 서버 애플리케이션은 클라이언트로부터 받은 요청 당 스레드를 할당하여 작업을 처리한다. 이러한 요청 당 스레드(thread-per-request) 방식에서는 서버의 처리 능력(낮은 응답시간 및 지연시간, 높은 처리량)이 스레드의 수에 따른 스레드의 작업 처리 능력에 비례한다. 서버가 기존 보다 더 많은 요청을 처리하도록 만들기 위해서는 서버의 가용 스레드를 늘려야 한다.

기존 JDK의 스레드는 java.lang.Thread 인스턴스를 말한다. 이 스레드를 플랫폼 스레드(platform thread)라고 한다. 플랫폼 스레드는 OS(운영 체제) 스레드 상에서 자바 코드를 실행하며 플랫폼 스레드의 전체 라이프사이클 동안 OS 스레드를 캡처(capture)한다. 플랫폼 스레드가 OS 스레드를 캡처한다는 것의 의미는 다음과 같다.

  • 하나의 플랫폼 스레드는 하나의 OS 스레드와 직접적으로 연결된다. 플랫폼 스레드와 OS 스레드는 1:1 매핑된다.
  • 플랫폼 스레드가 작업에 할당된다는 것은 OS 스레드와 관련된 시스템의 모든 리소스(CPU와 메모리)가 해당 플랫폼 스레드에 할당된다는 것을 말한다.
  • 플랫폼 스레드의 라이프 사이클과 OS 스레드의 라이프 사이클은 서로 동기화된다. 플랫폼 스레드와 OS 스레드는 시작과 종료를 함께한다.
  • 시스템 상의 리소스 한계로 인해 OS 스레드의 수는 제한된다. 따라서 플랫폼 스레드의 수도 제한된다.


애플리케이션이 사용할 수 있는 플랫폼 스레드의 수는 OS 스레드의 수로 제한된다. 플랫폼 스레드는 일반적으로 운영 체제에서 유지 관리하는 OS의 리소스(CPU, 메모리)를 사용하며 큰 규모의 스레드 콜 스택(call stack)을 갖는다. 콜 스택이란 메서드나 함수와 같은 서브루틴(subroutine) 호출에 필요한 데이터를 저장하는 스택 자료 구조이다. 콜 스택에 저장되는 주요 데이터는 현재 실행 중인 서브루틴의 재개를 위한 반환 주소(return address)이며, 이 외에 로컬 변수 데이터, 파라미터 데이터 등이 저장된다. 콜 스택이 사용하는 리소스는 메모리이며 시스템의 메모리 제한은 콜 스택 및 스레드 생성을 제한한다.

플랫폼 스레드는 CPU 집약적 연산과 IO 집약적 연산과 같은 모든 유형의 작업을 실행하는데 적합하지만 사용할 수 있는 리소스가 제한적이다. 플랫폼 스레드는 요청 당 스레드 모델에서 한계가 명확하다. 플랫폼 스레드 수의 최대 개수 제한은 OS 스레드 수와 메모리 두 가지 요소에 의해 영향을 받는다. 메모리, 네트워크 연결과 같은 다른 리소스가 고갈되기 전에 우선적으로 제한된 OS 스레드 수가 애플리케이션의 성능을 제한한다. 즉, 플랫폼 스레드는 애플리케이션의 처리량을 하드웨어가 지원할 수 있는 것보다 훨씬 낮은 수준으로 제한한다. 스레드 풀링(thread pooling)은 새 스레드를 시작하는데 드는 높은 비용을 피하는데 도움을 주지만 최대 스레드 수는 제한되어 있기 때문에 스레드 풀링이 OS 스레드에 의한 성능 제한을 해결할 수는 없다.

가상 스레드도 플랫폼 스레드와 마찬가지로 java.lang.Thread의 인스턴스이며 OS 스레드 상에서 코드를 실행한다. 그러나 가상 스레드는 플랫폼 스레드와 다르게 OS 스레드에 의해 제한되지 않는다. 가상 스레드에서 실행 중인 코드가 블로킹 IO 작업을 호출하면 자바 런타임은 해당 작업이 완료되어 이후 작업이 재개될 때까지 가상 스레드를 일시 중단(suspend)한다. 일시 중단된 가상 스레드와 연결된 OS 스레드는 다른 가상 스레드에 대한 작업을 수행할 수 있다.

플랫폼 스레드와 달리 가상 스레드는 콜 스택이 얕기 때문에 단일 HTTP 클라이언트 호출 또는 단일 JDBC 쿼리 정도만 수행하는 것이 일반적이다. 따라서 플랫폼 스레드는 IO 작업이 완료될 때까지 기다리는 작업과 같이 대부분의 시간을 블로킹된 상태로 소비하는 작업을 실행하는데 적합한 반면 장시간 실행되는 CPU 집약적인 작업에는 적합하지 않다.

플랫폼 스레드와 가상 스레드의 공통점과 차이점을 정리하면 다음과 같다.

  • 공통점
    • OS 스레드 상에서 코드를 실행한다.
    • java.lang.Thread의 인스턴스이다.
  • 차이점
    • 플랫폼 스레드는 OS 스레드에 의해 제한되는 반면 가상 스레드는 제한되지 않는다.
    • 플랫폼 스레드는 제한된 리소스이며 생성 비용이 많이 들고 메모리 사용량이 많아 무겁다. 따라서 적절한 스레드 풀링 과정을 통한 리소스 관리가 필요하다. 반면 가상 스레드는 플랫폼 스레드에 비해 생성 비용이 적게 들고 메모리 사용량이 적어 가볍다. 가상 스레드의 경우 풀링이 필요하지 않다.
    • 플랫폼 스레드는 콜 스택이 깊은 반면 가상 스레드는 콜 스택이 얕다.


스레드 로컬 변수

가상 스레드는 플랫폼 스레드와 마찬가지로 스레드-로컬 변수인 ThreadLocal을 지원하며 스레드-로컬 변수의 상속도 지원한다. 단일 JVM이 수백만 개의 가상 스레드를 지원할 수 있기 때문에 리소스 오버헤드 관점에서 스레드-로컬 변수 사용을 신중하게 고려해야 한다.

스레드의 작업 실행 컨텍스트 별로 데이터를 분리하기 위해 사용하는 스레드-로컬 변수는 가상 스레드에서도 유용하다. 더 안전하고 효율적으로 스레드 별 데이터 관리를 위해 ScopedValue를 사용할 수도 있다.

플랫폼 스레드와 달리 가상 스레드는 풀링되지 않는다. 따라서


피닝

피닝(pinning)


코틀린의 코루틴과 가상 스레드

요청 당 스레드 모델에서 코루틴과 가상 스레드의 장점


참고

Comments