블로그 포스트 등을 통해서 Java21 에 대한 내용들을 접하게 되었다. 그중에서 virtual thread 에 대한 글들을 많이 접할 수 있었다. virtual thread 가 기존 thread 보다 가볍고 성능이 우수하다는 내용을 접하면서 흥미가 생겼고, 이전에 I/O 작업을 처리하기 위하여 사용했던 파이썬의 asyncio 와도 유사한 느낌이 들어서 virtual thread 와 관련 내용을 정리해보려고 한다.
1. virtual thread 란?
virtual thread 는 기존 자바의 동시성 모델을 개선하고자 하는 Project Room 에 의해 시작된 경량화 스레드 기술로 JDK21 에 정식 기능으로 추가되었다. 기존의 방식과 비교하여 많은 동시성 작업에 대해서 처리량이 많고 대기시간이 짧도록 개선하였다. 기존의 자바 스레드가 OS 스레드와 일대일로 매핑되어 동작했던 것과는 달리 다대일로 매핑되어서 동작할 수 있도록 한다. 이로인해 다수의 virtual thread 를 JVM 에서 관리하는 적은 수의 OS 스레드로 처리할 수 있도록 한다.
2. virtual thread 의 구조 및 동작방식
- 기존 Thread 의 구조와 문제점
기존 자바 스레드는 플랫폼 스레드 또는 유저 스레드라고 불린다. 플랫폼 스레드 OS 스레드를 래핑한 것으로 플랫폼 스레드가 생성되면 JNI (Java Native Interface) 를 통해 커널 영역을 호출하고 OS 에 의해 생성된 커널 스레드와 일대일 매핑된다.
작업을 수행중인 스레드는 I/O, interrupt, sleep 등과 같은 상황이 되면 block/waiting 상태가 되는데, 이때 다른 스레드가 커널 스레드를 점유하여 작업을 수행하게 된다. 이때 기존에 동작중이던 스레드의 메모리를 이동하고 새로운 스레드를 할당하는 과정을 컨텍스트 스위치라고 한다.
이러한 스레드는 프로세스보다 작은 단위로 프로세스 내부에서 동작하는데, 프로세스에 비해 메모리 크기가 작아 생성 비용이 적고, 컨텍스트 스위치 비용이 저렴하기 떄문에 동시성 작업 처리를 위해서 많이 사용해왔다. 하지만 OS 스레드 자체가 생성 갯수에 제한이 있고 생성시마다 커널 영역과 통신을 해야하는 등의 이슈로 효율적으로 사용하기 위해 ThreadPool 방식으로 스레드를 사용해오고 있다.
그러나 이러한 구조는 웹 구조에서 문제점을 드러내고 있다. 스프링 MVC 의 API 구조에서는 Thread per Request 로 하나의 요청에 하나의 스레드를 할당하여 요청을 처리하고 있다. 그런데 처리량의 증가에 맞춰서 스레드를 무한정으로 늘릴 수 없기 때문에 금방 처리량의 한계에 도달하는 문제가 발생한다. 또한 I/O 작업을 수행하는 경우에는 I/O waiting 으로 인해 작업을 처리하는 시간보다 대기하는 시간이 길어지기도 한다.
- virtual thread 의 구조
virtual thread 는 기존 자바 스레드와는 달리 커널 스레드가 아닌 JVM 의 캐리어 스레드와 다대일로 매핑되며 OS 가 아닌 JVM 에 의해 스케줄링 된다. 기존의 스레드 구조가 커널 스레드와 플랫폼 스레드로 구성되어 이들이 일대일로 매핑되는 구조였다면, virtual thread 는 플랫폼 스레드와 매핑되는 가상 스레드 계층이 하나 더 있는 구조라고 생각하면 된다.
virtual thread 는 ForkJoinPool 에 의해서 캐리어 스레드와 매핑되어 작업을 처리한다. 캐리어 스레드는 플랫폼 스레드와 같이 OS 스레드와 매핑되어 작업을 처리하도록 하지만 사용자가 명시적으로 캐리어 스레드를 사용하는 것이 아니라 virtual thread 를 통해서 캐리어 스레드가 동작되게 된다.
캐리어 스레드는 workQueue 를 가지고 있다. workQueue 에는 virtual thraed 의 작업들이 들어있는데, 이들은 ForkJoinPool 에 의해서 스케줄링되어 캐리어 스레드에 할당되어 처리된다.
이때 처리되는 virtual thread 의 작업단위는 continuation 이다. continuation 은 동작중에 I/O, sleep, yield 등의 상황을 만나면 해당 continuation 은 heap 으로 이동된다. 그리고 새로운 작업이 ForkJoinPool 에 의해 스케줄링 되어 처리된다. 중단된 continuation 이 재실행되면 heap 에 있는 메모리를 다시 stack 으로 올리고 이어서 작업을 수행한다. 이 과정에서 continuation 이 yield 됨으로 non-blocking I/O 와 같이 실제 스레드 (캐리어 스레드) 가 중단되지 않고 이어서 다른 작업을 수행할 수 있게된다.
3. virtual thread 의 장점
기존 스레드 모델과 virtual thread 의 구조를 비교하면서 virtual thread 를 사용함으로 개선되는 기존 스레드의 문제점을 알 수 있다.
- 처리량 증가
우선 애플리케이션의 처리량을 증가시킬 수 있다. 기존 스레드 모델에서 Thread per Request 구조로 인해 한정된 OS 스레드를 사용하면서 처리량이 저하되는 문제가 있었다. 그러나 virtual thread 를 사용한다면 이러한 생성 갯수의 한계가 없기 때문에 처리량을 증가시킬 수 있다. 또한 virtual thread 의 생성과 스케줄링 과정이 JVM 내부에서 동작하기 때문에 OS 스레드와의 통신이 필요한 기존 스레드 모델에 비해 훨씬 높은 성능을 보인다.
- I/O blocking 개선
I/O waiting 으로 인한 blocking 문제도 해결할 수 있다. virtual thread 은 I/O waiting 이 발생하는 경우 캐리어 스레드가 blocking 되어 작업이 끝나기를 대기하는 것이 아니라 다른 virtual thread 의 작업을 할당받아서 작업을 수행할 수 있다. 이로인해서 기존의 blocking 문제를 개선할 수 있다.
- 기존 Thread 상속
이외에도 기존 Thread 를 상속받으면서 코드 이해와 사용에 큰 어려움이 없다는 것도 장점이다. 동시성 문제를 해결하기 위해서 Webflux 나 코루틴 등의 도입을 고민하는 경우가 있다. 하지만 이러한 방법들은 새로운 기술을 익혀야 하는 부담이 있는데, virtual thread 의 경우 기존 Thread 의 문법을 그대로 사용하면서 플랫폼 스레드를 사용할 것인지 virtual thread 를 사용할 것인지만 결정하면 되기 때문에 보다 부담이 덜하다.
4. virtual thread 사용시 주의사항
virtual thread 는 기존 스레드 모델의 성능을 개선하지만 그렇다고해서 만능은 아니다. virtual thread 를 도입할 때의 주의사항들은 다음과 같다.
- CPU bound 작업
앞서 본 것과 같이 I/O 작업에 대해서는 virtual thread 가 장점을 가진다. 하지만 CPU bound 작업은 다르다. CPU bound 작업은 어차피 플랫폼 스레드에서 동작해야 하기 때문에 컨텍스트 스위치가 발생하지 않는 상황에서 virtual thread 를 사용하는 것은 오히려 virtual thread 의 생성과 스케줄링으로 인해 성능이 더 떨어진다.
- Thread Local 메모리 이슈
virtual thread 는 개수에 제한이 없다. 그 말은 수백만개의 스레드를 동시에 운용할 수 있다는 것이다. 이때문에 virtual thread 는 항상 메모리를 작게 유지해야 한다. 가벼운 작업을 빠르게 스위칭하면서 처리하는 것이 virtual thread 의 컨셉이라고 생각하고 작업해야 한다.
- pinning 문제
virtual thread 에서 synchronized 나 parallelStream 과 같은 메서드를 사용하게 되면 캐리어 스레드 자체가 blocking 되어버려서 virtual thraed 가 스위칭 되지 못하고 고정되어 성능저하를 유발할 수 있다. 이때문에 내부 로직에서 이러한 작업이 없는지, 연동된 써드파티앱 등에 그러한 연산이 없는지 잘 고려해보아야 한다. 만약에 그러한 이슈가 있는 경우에는 ReentrantLock 을 통해서 회피할 수 있는 방법이 있다고 하니 필요한 경우에는 이러한 방법을 참고해 볼 수 있다.
[Reference]
- https://techblog.woowahan.com/15398/
'프로그래밍언어 > JAVA' 카테고리의 다른 글
[Java] Record (1) | 2024.06.15 |
---|---|
[JAVA] 람다식 (lambda expression) (1) | 2024.01.24 |
[JAVA] 자바 Thread-safe (0) | 2022.08.21 |
[JAVA] 메서드 참조 (::) (0) | 2022.04.14 |
[JAVA] 쓰레드의 동기화 (0) | 2022.03.15 |