Java의 Thread 그리고 JVM 21의 Virtual Thread - 1. 스레드 모델

Thread Model

커널 스레드 (Kernel Thread) 구현

커널 스레드를 이용하는 구현. 일대일 구현이라고도 한다. 커널 스레드는 운영체제의 커널에서 직접 지원하는 스레드이며, 스레드의 작업을 각 프로세서에 매핑하는 역할을 한다. 커널 스레드 각각은 커널의 복제본으로 생각할 수 있다. 멀티-스레딩을 지원하는 커널을 멀티스레드 커널이라 한다.

프로그램은 일반적으로 커널 스레드를 직접 사용하지 않고, 커널 스레드의 고수준 인터페이스인 경량 프로세스를 이용한다. (일반적으로 스레드라고 부르는 것.) 경량 프로세스는 각각 스레드의 도움을 받기 때문에 커널 스레드가 먼저 지원되어야 경량 프로세스도 존재할 수 있다. 경량 프로세스와 커널 스레드 사이의 관계가 1:1이며, 경량 프로세스는 각각 독립된 단위로 스케쥴링된다.

커널 스레드를 기반으로 구현되어있어 생성, 소멸, 동기화 등 다양한 스레드 연산이 시스템 콜로 이루어진다. 시스템 호출은 사용자 모드와 커널 모드 전환을 수반하기 때문에 실행 비용이 상대적으로 높다. 커널 스레드의 스케쥴링 비용은 주로 사용자 모드와 커널 모드 사이의 전환 비용에 의해 결정되며, 이 비용은 주로 인터럽트에 응답하고 실행 사이트의 데이터를 저장했다가 복원하는 비용이다. (후술하는 사용자 스레드에서도 똑같다고 이야기할 수 있지만, 복원 작업을 개발자가 한다면 시도해 볼 수 있는 절감 기법이 많이 있다.)

경량 프로세스 하나가 커널 스레드 하나에 매핑되므로 경량 프로세스는 일정 량의 커널 자원(스택 공간, …)을 소모한다. 따라서 시스템이 지원할 수 있는 경량 프로세스 개수에는 제한이 있다.

Kernel threads

사용자 스레드 (User Thread) 구현

사용자 스레드를 이용하는 구현을 1:N 구현이라고 한다. 좁은 의미의 사용자 스레드는 사용자 공간에서 구현되는 스레드 라이브러리를 의미한다. 따라서, 운영 체제 커널은 사용자 스레드의 존재와 구현 방법을 알 수 없다. 사용자 스레드의 생성, 소멸, 동기화, 스케쥴링은 모두 커널의 도움 없이 온전히 사용자 공간에서 처리한다. 매우 빠르고 저렴하여 더 많은 스레드를 지원할 수도 있어 일부 고성능 데이터베이스는 멀티스레딩을 사용자 스레드로 구현한다.

개념 상 커널의 지원을 못받는 단점이 존재하기도 한다. 운영 체제는 프로세서 자원을 프로세스에만 할당하기 때문에

등의 문제를 해결하기 어려운 모델이다. 사용자 스레드로 구현된 프로그램은 일반적으로 복잡하여 일반적인 어플리케이션에서는 잘 사용하지 않는다. Go, erlang에서 활용되면서 다시 사용하는 비율이 높아지고 있다.

User threads

하이브리드 구현

커널 스레드와 사용자 스레드를 함께 이용하는 하이브리드 구현도 있다. 위에서 언급된 두 모델이 공존한다. 사용자 스레드는 여전히 사용자 영역에 존재한다.
사용자 스레드 모델과 같이 생성, 소멸, 스케쥴링 비용이 저렴해지고 감당할 수 있는 동시성 규모가 커진다.

운영체제가 제공하는 경량 프로세스는 사용자 스레드와 커널 스레드 사이의 매핑을 담당한다. 커널이 지원하는 스레드 스케쥴링과 프로세서 매핑 기능을 사용할 수 있게되었다. 또한 사용자 스레드의 시스템 콜은 온전히 경량 프로세스에 의해 수행되므로 프로세스 전체가 완전히 블록될 위험이 크게 줄어든다.

Hybrid threads

Java의 Thread

Java의 스레드 구현

운영체제가 어떤 스레딩 모델을 제공하느냐가 자바 가상머신의 스레드가 매핑되는 방식에 큰 영향을 준다. 운영체제가 제공하지 않는 모델을 직접 구현되는 건 어려우므로 “The Java Virtual Machine Specification” 에서는 자바 스레드가 작동하는 스레딩 모델을 규정하지 않았다. 위에서 언급한 스레딩 모델은 스레드의 동시성 규모와 운영 비용에만 영향을 주며, 자바 프로그램의 코드 작성과 실행에는 관련이 없다.

Java Thread State Machine

Java21의 Virtual Thread

2017년, OpenJDK는 자바 스레드 모델의 한계를 보완할 공식 해법으로 룸 프로젝트를 출범한다. 그리고 결실을 맺어 JDK 21에 정식 반영했다. 초기에는 파이버(Stackful coroutines or user mode cooperatively scheduled threads)라고 불렀지만 혼동을 피하기 위해 Virtual Thread로 이름을 바꿨다.

Fiber

platform thread는 java.lang.Thread와 같은 것이다.

가상 스레드는 플랫폼 스레드와 N:1의 관계를 가진다. 가상 스레드 하나가 블록되면 플랫폼 스레드는 연결된 다른 가상 스레드의 실행을 계속할 수 있다. 가상 스레드의 이점은 I/O 작업이 많아서 스레드 전환이 자주 일어나는 상황에서 더욱 두드러진다.

새로운 동시성 모델에서 가상 스레드를 이용하는 코드는 실행 대상인 후속문과 스케쥴러로 나뉜다.

이처럼 분리된 형태는 스케쥴링 방식을 개발자가 직접 제어할 수 있다는 이점이 있다. 자바의 기존 스케쥴러도 재사용할 수 있으며, 실제로 룸의 기본 스케쥴러는 JDK 7에서 추가된 ForkJoinPool이다.