Thread
Java에서는 JDK 1.0부터 `Thread` 클래스를 제공해 멀티태스킹을 지원했습니다.(Thread) `Thread` 클래스를 통해 스레드를 생성할 수 있고 이 클래스를 상속받아 run() 메서드를 오버라이드하거나 `Runnable` 인터페이스를 구현해 `Thread` 클래스 생성자의 인자로 넘겨줌으로써 원하는 작업을 수행하는 스레드를 생성할 수 있습니다.
스레드의 상태는 다음과 같습니다. (Thread.State)
- New: 새로 생성된 스레드, 아직 시작되지 않음
- Runnable: 실행 가능 혹은 실행 중인 상태
- Blocked: 동기화에 의해 차단됨
- Waiting: 다른 스레드가 특정 액션을 수행하길 기다림
- Timed Waiting: 지정된 시간 동안 기다림
- Terminated: 실행 완료
다만 이렇게 `Thread`를 이용해 멀티태스킹을 구현하면 여러 스레드가 동시에 같은 자원에 접근할 때 발생하는 동기화 문제를 비롯해 멀티 스레딩에서 발생하는 문제들을 직접 해결해야합니다.
Concurrent Package
`Concurrent` 패키지는 스레드의 추상화 레벨을 높여 보다 쉽고 효율적으로 멀티 스레드를 관리할 수 있도록 해줍니다.(Concurrent Package description)
`Concurrent` 패키지는 JDK 5부터 추가됐으며 효율적인 스레드 관리를 위해 `스레드 풀` 을 이용하는 클래스들을 제공합니다. 작업마다 하나의 스래드를 생성하고 작업이 끝나면 스레드를 제거했던 기존 `Thread`를 사용하던 방식과 달리 작업을 큐에 넣고 스레드 풀에서 관리하는 스레드가 작업을 큐에서 꺼내 처리한 뒤 작업이 완료되면 스레드 풀로 반환되는 방식입니다. 이렇게 하면 작업마다 스레드를 생성 및 제거하는 기존의 방법보다 스레드 생성/제거 비용을 절약할 수 있습니다. (스레드 풀을 생성, 관리하는 ThreadPoolExecutor)
`Concurrent` 패키지의 주요 구성요소 몇 가지를 살펴보면 다음과 같습니다.
- Executor: 스레드 풀, 비동기 I/O 및 경량 작업 프레임워크 등과 같은 스레드와 유사한 하위 시스템 정의 표준화 인터페이스
- Concurrent Collections: 동시성을 고려해 설계된 컬렉션들(`ConcurrentHashMap`, `BlockingQueue`, ...)
- Synchronizers: 동시성 문제를 해결하기 위해 사용되는 유틸리티
또한 Js의 `Promise`와 비슷한 Futrue 클래스를 이용하면 비동기 연산 결과를 나타낼 수 있습니다. `ExecutorService`로 작업을 제출(submit)하면 `Future`객체가 반환되고 이 객체를 통해 작업의 완료상태를 확인하고 결과를 얻을 수 있습니다.
여기까지 읽었을 때 `Concurrent` 패키지가 어디에 쓰면 좋을지 떠오르는 사용처가 있습니다.
자바의 대표적인 웹 프레임워크의 spring MVC는 thread per request 방식으로 다중 클라이언트의 요청을 처리합니다. 또 웹서버는 다중 사용자가 동시에 접근하는 데이터베이스 요청에 대해 동기화를 고려해줘야 합니다.
여러 개의 스레드를 효율적으로 관리하며 이로 인해 생기는 문제들을 쉽게 해결해 주는 `Concurrent` 패키지는 멀티스레딩 웹서버 구현을 쉽게 만들어 줍니다.
Virtual Thread
JDK 21에 새로 추가된 Virtual Thread는 JVM 내부에서 생성되고 관리되는 경량화 가상 스레드입니다.
카카오 안정수님의 발표와 블로그를 참조했는데 너무 읽기 쉽게 정리가 잘 되어있어서 원문을 보는 것을 추천합니다. 이 글에서는 아주 간단하게 요약해보고자 합니다.
- kakao tech meet 발표영상: JDK 21 신기능 Virtual Thread 알아보기 (안정수 James)
- 발표자의 발표 내용 블로그 글:
기존 자바의 스레드는 OS스레드를 래핑한 플랫폼 스레드를 사용하는 방식입니다. 따라서 자바 애플리케이션은 플랫폼 스레드를 이용했고 플랫폼 스레드는 OS에 종속적으로 개수가 제한적이고 비용이 비싸다는 단점이 있습니다. 이러한 문제를 해결하기 위해 위에서 알아봤듯 스레드 풀을 만들어 비용을 절약했습니다. 여기까지가 위에서 살펴본 내용입니다.
위에서 언급했듯 Spring MVC는 thread per request 방식으로 요청을 처리합니다. 스레드 풀을 이용한다 해도 결국 애플리케이션의 처리량(throughput)은 스레드 풀과 큐의 한계를 넘을 수 없습니다. 스레드 풀의 모든 스레드에 요청이 할당되고 해당 요청들이 무거운 I/O작업을 처리해야 하는 상황을 생각해 봅시다. 요청은 큐에 쌓여만 가는데 스레드 풀의 모든 스레드에는 요청이 할당되어 있고 요청이 할당된 스레드들은 I/O작업으로 `blocking`된 상태면 컴퓨터의 가장 중요한 자원 CPU는 쉬게됩니다.
이러한 상황을 해결할 수는 없을까? 에 대한 답 중 하나로 `WebFlux`가 대표하는 `Non-blocking` 방식이 있습니다. 하지만 WebFlux의 `Reactive` 프로그래밍은 기존 코드와 호완성, 디버깅의 어려움, 개발 난이도 등의 이슈가 있습니다.
위와 같은 배경에서 아래와 같은 목적을 갖고 `Virtual Thread`가 등장합니다.
- 자원을 최대한 활용하여 높은 처리량의 서버를 제작하는 것
- 가상 스레드에서 blocking이 발생하면 JVM 내부에서 스케줄링을 통해 플랫폼 스레드에 다른 가상 스레드를 할당해 자원을 최대한 활용한다.
- 기존 코드와 호완성을 유지한다.
가상 스레드를 이용해 요청을 처리하는 방법을 보면 가상 스레드가 왜 등장했는지 쉽게 이해할 수 있습니다. (Virtural Thread란 무엇일까? (1)의 그림 자료를 참고)
기존에는 요청(`task`)을 플랫폼 스레드에 직접 할당해 줬다면 가상 스레드를 사용한 경우 요청을 처리하는 스레드(`Virtual Thread` 이하 가상 스레드)와 플랫폼 스레드(`Carrier Thread`)를 분리해 JVM 내부에서 스케줄링을 통해 가상 스레드와 캐리어 스레드를 매칭하는 방식으로 작동합니다.
쉽게 생각하면 아래와 같이 이해할 수 있습니다.
- 받은 요청은 가상 스레드로 만든다.
- JVM 내부 스케줄링으로 캐리어 스레드(플랫폼 스레드)에 가상 스레드를 할당해 준다.
- 캐리어 스레드에서 실행 중인 가상 스레드가 blocking 작업을 수행하면 가상 스레드 스위칭으로 자원을 최대한 활용한다.
글의 Virtual Thread 섹션 처음에 가상 스레드는 경량 스레드라고 설명했습니다. 가상스레드의 메타 데이터 사이즈, 메모리, 컨텍스트 스위칭 비용은 기존 스레드(플랫폼 스레드)에 비해 아주 작습니다.
'Languages > Java' 카테고리의 다른 글
M1 MAC에서 JDK 버전 관리 (0) | 2023.12.26 |
---|---|
Java로 알아보는 비트 연산과 시프트 연산 (0) | 2023.08.22 |