본문 바로가기
Backend/Java

Virtual Thread

by DooDuZ 2024. 8. 5.

신기술이란...

Java 21부터 정식 릴리즈된 가상 스레드에 대해 알아보려고 합니다. 우아한 테크 채널의 발표 영상과 우아한 기술 블로그의 글을 많이 참조했습니다. 기술적으로 정확한 정보를 전달해 준 우아한 테크와 달리 이 글은 제 추측이 많이 가미되었습니다. 부디 정보로 받아들이기보다는 이런 경험과 생각을 한 사람이 있구나 정도로 봐주시면 감사하겠습니다.
 
[참고 링크]
우아한 테크 세미나 - Java의 미래, 가상 스레드
우아한 기술 블로그 - Java의 미래, Virtual Thread
JEP 444: Virtual Threads

Java의 미래, Virtual Thread | 우아한형제들 기술블로그

JDK21에 공식 feature로 추가된 Virtual Thread에 대해 알아보고, Thread, Reactive Programming, Kotlin coroutines와 비교해봅니다.

techblog.woowahan.com

 
 

Platform Thread

신기술이라고 무작정 도입할 순 없죠. 원래 기술과 어떻게 다른지, 무엇이 더 좋은지 비교할 필요가 있습니다. 가상 스레드를 논하기에 앞서 지금까지 사용하던 Java의 스레드(Platform Thread)를 알아보겠습니다. 

 

더보기

JEP 444: Virtual Threads

Today, every instance of java.lang.Thread in the JDK is a platform thread. A platform thread runs Java code on an underlying OS thread and captures the OS thread for the code's entire lifetime. The number of platform threads is limited to the number of OS threads.

 
JVM의 스레드는 기본적으로 OS 스레드와 1:1 매칭되어 실행됩니다. 수십, 수백 개의 스레드를 동시에 run 하는 경우 모든 스레드가 동시에 실행되는 것처럼 보이지만 실제로는 OS의 스케쥴링에 의해 여러 스레드가 번갈아가며 실행됩니다. 위 그림처럼 세개의 OS 스레드와 JVM 스레드가 매칭되어 있다고 가정해 봅시다.
 

이때 Thread 1이 Blocking, wait이나 Sleep 상태에 들어가게 되면 JVM은 대기 중인 JVM 스레드와 OS 스레드를 매칭하려 시도합니다. 이때 JNI를 통해 네이티브 코드를 실행하고 커널에 작업을 요청하는 것을 System Call이라고 합니다.
 

OS 스레드와 새로운 JVM 스레드가 매칭되고 Thread 3이 실행됩니다. 이처럼 실행할 스레드를 교체하는 작업을 Context Switch라고 부릅니다. CS공부를 하다 보면 반드시 한 번쯤은 듣게 되는 단어인데, OS 레벨에선 스레드뿐 아니라 프로세스 단위의 컨텍스트 스위치 또한 다루는 것 같습니다. 일단 전 응애 개발자이므로... JVM 스레드의 컨텍스트 스위칭은 JNI를 통한 시스템 콜이 필요하다 정도로 생각하기로 했습니다.
 

Virtual Thread

그렇다면 가상 스레드는 어떨까요? 아래는 공식 문서에 적혀있는 가상 스레드에 대한 설명입니다.

더보기

A virtual thread is an instance of java.lang.Thread that runs Java code on an underlying OS thread but does not capture the OS thread for the code's entire lifetime. This means that many virtual threads can run their Java code on the same OS thread, effectively sharing it. While a platform thread monopolizes a precious OS thread, a virtual thread does not. The number of virtual threads can be much larger than the number of OS threads.

번역기를 돌려보면, OS Thread 하나에서 여러 개의 가상 스레드가 실행될 수 있다는 내용으로 보입니다. 공식 문서에서 본문 전체를 읽어본다면 OS Thread와 1:1 매칭되던 Platform Thread와 달리 가상 스레드는 N:M으로 매칭되어 실행된다는 내용이 이어지는 걸 확인할 수 있습니다.
 

 
가상 스레드는 Heap에 생성되어 플랫폼 스레드를 통해 실행됩니다. 이때 JVM은 내부 스케쥴러로 ForkJoinPool을 사용하게 됩니다. OS의 스케쥴링을 통해 OS스레드를 래핑 하여 플랫폼 스레드가 사용했던 것처럼 JVM 내부 스케쥴링을 통해 가상 스레드를 플랫폼 스레드를 통해 실행하는 방식입니다. 위에서 System CallContext Switch를 언급한 이유가 여기에 있습니다. OS와 상호작용 없이 JVM 내부적으로 스레드를 park, unpark 하며 실행하기 때문에 컨텍스트 스위칭 비용이 상대적으로 적게 발생합니다.
 

더보기

Preserving the thread-per-request style with virtual threads

 

To enable applications to scale while remaining harmonious with the platform, we should strive to preserve the thread-per-request style. We can do this by implementing threads more efficiently, so they can be more plentiful. Operating systems cannot implement OS threads more efficiently because different languages and runtimes use the thread stack in different ways. It is possible, however, for a Java runtime to implement Java threads in a way that severs their one-to-one correspondence to OS threads. Just as operating systems give the illusion of plentiful memory by mapping a large virtual address space to a limited amount of physical RAM, a Java runtime can give the illusion of plentiful threads by mapping a large number of virtual threads to a small number of OS threads.

 
또한 공식 문서에선 가상 스레드가 thread-per-request 구조를 유지할 수 있게 해준다고 안내하고 있습니다. 가상스레드는 Heap에 할당되므로 OS스레드의 개수만큼만 실행될 수 있는 플랫폼 스레드와 다르게 이론상 수십만 개도 존재할 수 있습니다. 이로 인해 스레드 풀 관리에서 비교적 자유로워질 수 있습니다.
 
 

주의점

스레드 수 제한, 컨텍스트 스위칭 비용 등 위에 기재되었거나 생략된 많은 장점들에도 불구하고 가상 스레드를 사용한다면 주의해야 할 점들이 있습니다. 
 
첫째로 스레드 개수에 제한이 없다는 게 되려 성능 이슈를 일으킬 수 있다는 점입니다. 우아한 테크 세미나에선 이를 '배압 조절' 기능이 없다고 표현한 것 같은데요. 스레드가 무한정 늘어나면 그만큼 CPU 자원을 나눠 사용하기 때문에 처리 효율이 매우 낮아지게 됩니다. 이때 정도가 과하면 하드웨어 이슈까지도 생길 수 있는 모양입니다.
 
둘째로 Pinned Issue입니다. 가상 스레드를 실행하는 플랫폼 스레드를 캐리어 스레드라고도 부르는데, 가상 스레드는 캐리어 스레드로의 unpark와 heap으로의 park를 반복하며 효율적으로 실행됩니다. 만약 synchronized가 호출되어 캐리어 스레드가 block 되면 가상 스레드가 heap으로 park 되지 못하는 상황이 발생하고 이를 Pinned Isuue라고 합니다. 따라서 네이티브 메서드 호출, parallelStream 등의 사용을 피하고 synchronized의 경우 가능하면 Reentrant Lock 사용으로 대체해야 한다고 합니다.
 
셋째로 Mysql과의 호환 문제가 있는데, 이는 MySQL 9.0.0에서 어느정도는 완화된 것으로 보입니다.

더보기
  • Synchronized blocks in the Connector/J code were replaced with ReentrantLocks. This allows carrier threads to unmount virtual threads when they are waiting on IO operations, making Connector/J virtual-thread friendly. Thanks to Bart De Neuter and Janick Reynders for contributing to this patch. (Bug #110512, Bug #35223851)

Connector/J의 동기화 블록이 Reentrant Lock으로 대체되었다는 내용을 확인할 수 있습니다.
 
넷째로 ThreadLocal 사용에 대한 내용입니다. 이는 제가 정확하게 이해하지 못한 내용인데 이해한 바를 설명해 보면 다음과 같습니다.
 
1. 가상 스레드가 heap에 park된 후 unpark 되는 과정에서 원래 실행된 캐리어 스레드가 아닌 다른 캐리어 스레드에서 실행될 수 있다.
2. 스레드 로컬은 캐리어 스레드에 할당되어 있으므로, 이 경우 스레드 로컬에서 사용하던 값을 복사해서 가져와야 하므로 오버헤드가 발생할 수 있다.
 
사용하는 스레드 로컬의 크기가 커질 수록 성능에 큰 영향을 미치는 것 같습니다. 이는 제 추측이 많이 가미된 것이고 공식 문서에서 말하는 스레드 로컬에 대한 내용은 아래와 같습니다.

더보기

Thread-local variables

 

Virtual threads support thread-local variables (ThreadLocal) and inheritable thread-local variables (InheritableThreadLocal), just like platform threads, so they can run existing code that uses thread locals. However, because virtual threads can be very numerous, use thread locals only after careful consideration. In particular, do not use thread locals to pool costly resources among multiple tasks sharing the same thread in a thread pool. Virtual threads should never be pooled since each is intended to run only a single task over its lifetime. We have removed many uses of thread locals from the JDK's java.base module in preparation for virtual threads in order to reduce memory footprint when running with millions of threads. The system property jdk.traceVirtualThreadLocals can be used to trigger a stack trace when a virtual thread sets the value of any thread-local variable. This diagnostic output may assist with removing thread locals when migrating code to use virtual threads. Set the system property to true to trigger stack traces; the default value is false. Scoped values (JEP 429) may prove to be a better alternative to thread locals for some use cases.

 
마지막으로 Pooling을 하지 않을 것을 권장한다는 점입니다. 가상 스레드는 매우 가볍기 때문에 pooling을 통한 사용보다는 언제나 새로 생성해서 사용하고 GC를 통해 메모리에서 해제되는 것을 권장합니다. 이에 대한 내용은 아래와 같습니다.

더보기

Implications of virtual threads

 

Virtual threads are cheap and plentiful, and thus should never be pooled: A new virtual thread should be created for every application task. Most virtual threads will thus be short-lived and have shallow call stacks, performing as little as a single HTTP client call or a single JDBC query. Platform threads, by contrast, are heavyweight and expensive, and thus often must be pooled. They tend to be long-lived, have deep call stacks, and be shared among many tasks.

 
이상 Java 가상 스레드에 대한 내용이었습니다. 개인 프로젝트에 가상 스레드를 적용하며 겪은 문제들을 포스팅하려다 먼저 개념 정리가 필요할 것 같아 구태여 잘 알지도 못하는 내용을 포스팅해 봤습니다. 틀린 내용이 있다면 댓글로 지적해 주세요! 언제나 환영입니다!

'Backend > Java' 카테고리의 다른 글

Serial GC / Parallel GC / G1 GC  (0) 2024.06.19
JVM Stack / Heap - GC  (0) 2024.06.19
Java Compile  (2) 2024.06.07