📌 자바 제공하는 스레드 vs 운영체제 스레드
자바에서 스레드를 사용할 때에는 Thread객체를 생성하고, 작업할 내용을 프로그래밍 한다.
start()메서드가 실행되면 쓰레드 객체는 새로운 운영체제 스레드를 만들어달라고 요청하고, JVM에 요청해 정해진 크기의 스택 공간을 할당받아 로컬변수를 저장한다.
즉, Thread 객체는 운영체제의 스레드 1개가 담당하여, CPU의 core에 의해 스케쥴링된다.
이렇게 운영체제 스레드와 1:1로 맵핑되는 스레드를 “Platform Thread”라고 부른다.
이러한 Platform Thread는 OS Thread와 1:1로 매칭되기 때문에 많은 비용이 든다.
요약
- CPU 스레드 스케쥴링에 대한 모든 책임을 운영체제가 가지고 있다.
- Platform Thread는 운영체제 스레드 1대와 매칭되고 고정된 크기의 스택을 사용한다.
- 운영체제 스레드는 제한된 리소스이다.(무제한으로 사용할 순 없다)
- PlatformThread를 운영체제 스레드와 1:1로 매칭하는 것은 비용이 많이 발생하는 일이다.
📌 가상스레드
여기서 JDK21에서 가상스레드가 등장했다.
가상스레드는 JVM에 의해 스케쥴링되고, 고정된 크기의 스택을 가지지 않는다.
마치 객체가 힙에 저장되고 필요없어지면 GC에 의해 삭제되는 것처럼, 가상스레드 또한 사용되지 않는다면 JVM의 GC과정을 통해 제거된다,
즉, 모든 가상스레드는 JVM에 의해 관리되고, 운영체제는 이러한 가상 스레드에 대해 전혀 알지 못하는 상태이다.
결과적으로, 생성하고 관리하는데 많은 비용이 드는 Platform스레드와 달리, 가상스레드는 낮은 비용으로 많은 양을 생성할 수 있다.
가상 스레드가 생성되면, 플랫폼 스레드 풀에서 비어있는 PlatformThread에 마운트(연결) 시킨다.
가상스레드가 PlatformThread에 연결된 상태를 CarrierThread라고 부른다.
가상 스레드 작업이 끝나면 JVM은 CarrierThread의 마운트를 해제하고 GC에 의해 가상 스레드는 없어진다. 그리고 CarrierThread는 마운트가 해제되면서 다시 PlatformThread가 되고, 다른 가상스레드를 연결받을 준비가 된다.
만약 가상 스레드 작업이 끝나지 않고, Blocking과 같이 기다리는 상태가 된다면?
이때는 현재 진행중인 상태를 Heap영역에 저장하고, 마운트를 해제한다. 힙 영역에는 가상스레드의 명령어 포인터(IP)와 스택 상태가 저장된다.
요약
- 가상스레드는 단순히 작업을 실행하는 자바 객체와 같다.
- 그래서 많은 양의 가상스레드를 생성할 수 있고, 한정된 개수의 PlatformThread에 스케쥴링 시킨다.
- JVM은 이 작업을 실행하기 위해, Platform Thread 풀에서 다른 가상스레드와 연결되지 않은 Platform Thread에 연결시키고, 이때 연결된 스레드를 Carrier Thread라고 부른다.
- Carrier Thread는 작업을 하기위한 stack영역을 가지고 있다.
- 만약 Blocking되는 구간이 존재하면, 명령어포인터와 스택 상태를 힙영역에 저장하고 다른 가상 스레드 마운트하여 실행한다. → 스케쥴링의 개념과 같고 JVM이 이러한 스케쥴링을 담당한다.
- 모든 스케쥴링 작업은 JVM이 담당하기 때문에, 개발자는 이러한 과정을 제어할 순 없다.
📌 가상스레드의 성능
그렇다면 가상스레드를 사용할 때 어떠한 이점이 있을까?
만약 가상스레드가 CPU연산만 수행한다면 성능에 큰 차이는 없다.
하지만 가상스레드로 실행하려는 코드에 오랜 시간 기다려야하는 연산(Blocking operations, I/O)이 포함되어 있다면, 가상 스레드는 성능 측면에서 유리할 수 있다.
- 가상 스레드는 교체할 때 Context Switche(문맥 교환)비용이 없다. 운영체제가 스케쥴링에 참여하지 않기 때문이다.
- 블로킹된 가상 스레드 연결을 해제하고, 새로운 가상 스레드를 생성 후 마운트 하는 과정은 조금의 부하가 있지만 운영체제의 문맥교환보다 훨씬 낮은 비용이다.
가상 스레드 | 플랫폼 스레드 | |
문맥 교환 비용 | 낮음 | 상대적으로 높음 |
메모리 사용 | 낮음 | 상대적으로 높음 |
I/O 블로킹에 대한 시간 낭비 | 낮음 | 상대적으로 높음 |
요약
- 기존에 I/O가 발생하면 PlatformThread는 정지 된다.
- 하지만 가상쓰레드의 경우 I/O가 발생하면 다른 가상스레드가 PlatformThread에 매칭되어 실행된다.
- 코드는 동기방식으로 작성되었지만, 실제 동작은 비동기처럼 수행된다.
- 응답시간 측면에서는 기존과 똑같지만, I/O블로킹 발생 시 처리량 측면에서 유리하다.
📌 가상스레드 사용 시 주의점
Pinned Thread
가상스레드가 캐리어 스레드에 고정되어서, 다른 가상 스레드를 실행하지 못하는 상황
- synchronized 블록을 사용할 경우에 I/O블로킹이 발생하는 경우, JVM이 가상스레드를 캐리어스레드에 고정시켜버리므로, 가상스레드의 블로킹이 끝날 때까지 플랫폼 쓰레드도 같이 블로킹 된다.
-> synchronized 블록을 최소화하고 Lock을 사용해서 피해야 한다.
-> JDBC 드라이버(라이브러리)를 사용하면 Pinned Thread가 발생한다. 사용하는 라이브러리에서 Pinned Thread가 발생하는지 꼭 확인이 필요하다. - 네이티브 메서드 혹은 foreign함수를 사용하는 경우 발생
마지막으로 가상 스레드는 ThreadLocal을 지원한다. 단, 가상스레드는 무수히 많이 만들 수 있기 때문 메모리를 너무많이 사용하지 않도록 주의해야한다.(너무 많이 생성하면 결국 이것도 많은 메모리를 언젠가는 차지할 것임)
또한 가상스레드의 생성비용이 낮기 때문에 풀링방식을 사용하지 않고 필요하면 생성해서 사용하는 방식을 권장한다고 한다.
학습자료
- https://openjdk.org/jeps/444(jdk21문서)
- Java 멀티스레딩, 병행성 및 성능 최적화 - 전문가 되기(Udemy강의)
'동시성 프로그래밍' 카테고리의 다른 글
모니터와 자바 동기화(synchronized) (0) | 2024.04.06 |
---|---|
[동시성 프로그래밍] 동기 vs 비동기 vs Blocking vs Non-blocking (0) | 2024.01.08 |
[자바 멀티쓰레드] CPU와 캐시메모리에 따른 데이터 문제 (0) | 2023.09.07 |
[자바 멀티쓰레드] 동기화 기법 (0) | 2023.09.07 |
[자바 멀티쓰레드] 경쟁상태와 임계영역 (0) | 2023.09.07 |