Virtual Thread์ ๊ธฐ๋ณธ ๊ฐ๋ ์ดํดํ๊ธฐ
Virtual Thread์ ๊ธฐ๋ณธ ๊ฐ๋ ์ดํดํ๊ธฐ ๊ด๋ จ
JDK์ ์ ์ ๋์ ๋ Virtual Thread๋ ๊ธฐ์กด์ KLT(kernel-level thread)์ ULT(user-level thread)๋ฅผ 1:1 ๋งคํํ์ฌ ์ฌ์ฉํ๋ JVM์ ์ค๋ ๋ ๋ชจ๋ธ์ ๊ฐ์ ํ, ์ฌ๋ฌ ๊ฐ์ ๊ฐ์ ์ค๋ ๋๋ฅผ ํ๋์ ๋ค์ดํฐ๋ธ ์ค๋ ๋์ ํ ๋นํ์ฌ ์ฌ์ฉํ๋ ๋ชจ๋ธ์ ๋๋ค. ์ด ๊ธ์์๋ Virtual Thread๊ฐ ๊ธฐ์กด ์ค๋ ๋ ๋ชจ๋ธ๊ณผ ์ด๋ค ์ ์ด ๋ค๋ฅธ์ง ์์๋ณด๊ฒ ์ต๋๋ค.
JNI
Java Native Interface(์ดํ JNI)๋ C, C++์ฒ๋ผ ์ธํฐํ๋ฆฌํฐ ์์ด OS๊ฐ ๋ฐ๋ก ์ฝ์ ์ ์๋ ํํ์ ๋ค์ดํฐ๋ธ ์ฝ๋๋ฅผ JVM์ด ํธ์ถํ ์ ์๊ฒ ํ๋ ์ธํฐํ์ด์ค๋ค. ์ฝ๊ฒ ๋งํด, JVM์์ ๋ค๋ฅธ ์ธ์ด๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ํ๋ค. ์ด JNI ๋๋ถ์ Java๊ฐ ๋จธ์ ํ๋ซํผ์ ์๊ด์์ด ๋์ํ ์ ์๋ค. ์ด ํธ์ถ์ Java์์ ๋ฉ์๋ ์์ native
ํค์๋๋ฅผ ๋ถ์ฌ ํด๋น ๋ฉ์๋๊ฐ JNI๋ฅผ ์ฌ์ฉํจ์ ๋ํ๋ธ๋ค.
์ง์ ์ฌ์ฉํ๋ฉด์ ์ดํดํด ๋ณด์(macOS ๊ธฐ์ค).
hoyoungjni๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฅผ ๋์ ์ผ๋ก ์ฝ์ด์ค๊ฒ ํ๊ณ , hyNativeMethod์ ๋ฉ์๋๋ JNI๋ฅผ ์ฌ์ฉํ๋๋ก ์ ์ธํ๋ค.
package org.example;
public class HoyoungJNI {
public HoyoungJNI() {
}
private native void hyNativeMethod();
public static void main(String[] var0) throws Exception {
HoyoungJNI var1 = new HoyoungJNI();
var1.hyNativeMethod();
}
static {
System.loadLibrary("hoyoungjni");
}
}
hyNativeMethod๋ฅผ ๊ตฌํํด ๋ณด์. ํค๋ ํ์ผ์ ๋ง๋ ๋ค.
javac HoyoungJNI.java
javah -classpath ${๊ฒฝ๋ก} org.example.HoyoungJNI
#include <jni.h>
#ifndef _Included_org_example_HoyoungJNI
#define _Included_org_example_HoyoungJNI
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_org_example_HoyoungJNI_hyNativeMethod
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
์ด๋ฅผ ๊ตฌํํ๋ค.
#include <jni.h>
#include "org_example_HoyoungJNI.h"
JNIEXPORT void JNICALL Java_org_example_HoyoungJNI_hyNativeMethod(JNIEnv *env, jobject obj) {
printf("JNI๋ ์ด๋ ๊ฒ ๋์ํด์");
}
์ปดํ์ผํ๋ค.
gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/darwin -I"${๋ง๋ javahํค๋ํ์ผ๊ฒฝ๋ก}" -shared -m64 ${๊ฒฝ๋ก}/HoyoungJNI.c -o libhoyoungjni.dylib
#
# The filename of a dynamic library normally contains the libraryโs name with the lib prefix and the .dylib extension
.Dynamic Library Design Guidelines์ ๋ค์ ๊ท์ฝ์ ๋ฐ๋ผ ์์ lib
์ ๋ค์ .dylib
๋ฅผ ์ ์ธํ hoyoungjni
๊ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ด๋ฆ์ผ๋ก ์ธ์๋๋ค.
๊ณต์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ์ผ์ ๋ง๋ค์์ผ๋, ํ๊ฒฝ๋ณ์๋ฅผ ์ฃผ์ ํ์ฌ ์คํํด ๋ณด์.
# JNI๋ ์ด๋ ๊ฒ ๋์ํด์
java -Djava.library.path=${dylibํ์ผ๊ฒฝ๋ก} -classpath ${classpath} org.example.HoyoungJNI
์ฆ, ์ด๋ ๊ฒ JVM์ JNI๋ฅผ ์ฌ์ฉํ์ฌ ๋ณ๋ ์ธํฐํ๋ฆฌํฐ ์์ด C๋ก ์์ฑ๋ ์ฝ๋๋ฅผ ์คํํ๋ค.
Java ์ค๋ ๋
Java๋ java.util.concurrent.ExecutorService
๋ฅผ ๋์ด JVM ๋ด๋ถ์์ ์ค๋ ๋๋ฅผ ๊ด๋ฆฌ/์คํํ๋ค. ์ฌ๋ฌ ExecutorService
์ค ThreadPoolExecutor
๋ก ์ค์ ์ค๋ ๋๊ฐ ์คํ๋๋ ๋ถ๋ถ๋ง ๊ฐ๋จํ ์ดํด๋ณด์.
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
10, 10, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()
);
threadPoolExecutor.submit(() -> {});
submit
์ ํ๋ฉด ๋ฌด์จ ์ผ์ด ์ผ์ด๋๋ ๊ฑธ๊น?
๋ค์์ ThreadPoolExecutor
์ execute
ํจ์์ ์ผ๋ถ์ด๋ค.
/**
* java.util.concurrent.ThreadPoolExecutor.java
*/
public void execute(Runnable command) {
/* ... ์๋ต ... */
int c = ctl.get(); // ํ์ฌ RUNNUNG ์ํ์ธ ์ค๋ ๋ ์๋ฅผ ๊ฐ์ ธ์ค๊ณ
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true)) // ํ ์๋ณด๋ค ์์ผ๋ฉด ์์ปค์ ์ถ๊ฐํ๋ค.
return;
c = ctl.get();
}
/* ... ์๋ต ... */
}
private boolean addWorker(Runnable firstTask, boolean core) {
// ThreadPoolExecutor๊ฐ ์คํํด๋ ๋๋ค๊ณ ํ๋จํ๋ฉด
if (workerAdded) {
container.start(t); // ์คํํ๋ค.
workerStarted = true;
}
}
์์ปค์ ์ถ๊ฐ๋ ์ค๋ ๋๋ ๊ฒฐ๊ตญ Thread.start
๋ฅผ ์คํํ๋ค.
/**
* java.lang.Thread.java
*/
private native void start0(); // ์ค์ ์คํ์ ๊ฒฐ๊ตญ JNI๋ฅผ ํตํ๋ค.
public void start() {
synchronized (this) {
// zero status corresponds to state "NEW".
if (holder.threadStatus != 0)
throw new IllegalThreadStateException();
start0();
}
}
์ฆ, ExecutorService
์ ์ค์ผ์ค๋ง ์ ์ฑ
์ ๋ฐ๋ผ JNI๋ก ์ค๋ ๋๋ฅผ ์คํํ๋ ๋ฐฉ์์ด๋ค.
JDK 21์ ๊ธฐ์ค์ผ๋ก ์ดํด๋ณด์.
(
openjdk/jdk21
-/src/java.base/share/native/libjava/
Thread.c
)
static JNINativeMethod methods[] = {
{"start0", "()V", (void *)&JVM_StartThread},
{"setPriority0", "(I)V", (void *)&JVM_SetThreadPriority},
/* ... ์๋ต ... */
}
start0
๋ JVM_StartThread
๋ฉ์๋์ด๊ณ JavaThread
๋ฅผ ์์ฑํ๋ค.
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
/* ... ์๋ต ... */
native_thread = new JavaThread(&thread_entry, sz);
/* ... ์๋ต ... */
JVM_END
JavaThread
๋ Thread
์ ํ์ ํด๋์ค์ด๋ค.
class JavaThread: public Thread {
friend class VMStructs;
friend class JVMCIVMStructs;
friend class WhiteBox;
๊ฒฐ๊ตญ, Java ๋จ์ ExecutorService
๋ฅผ ํตํด ์ค์ผ์ค๋ง๋๋ ์ฌ๋ฌ java.lang.Thread
๊ฐ์ฒด๋ JVM์ ์กด์ฌํ๋ start0 ํจ์๋ฅผ JNI๋ฅผ ํตํด ํธ์ถํ๊ณ , ๊ฐ ๋จธ์ OS์ ๋ง๊ฒ ์ค์น๋ JVM์ ์ปค๋ ์ค๋ ๋๋ฅผ ๋ง๋ค์ด ์คํํ๋ค. ์ด๋ฌํ ๋ค์ดํฐ๋ธ ๋ฉ์๋ ํธ์ถ์ JVM ๋ด์์ ์คํ๊ณผ ๋ถ๋ฆฌ๋์ด ์๋ ๋ค์ดํฐ๋ธ ๋ฉ์๋ ์คํ์ ์ฌ์ฉํ๋ค.
์ฆ, ์ค์ผ์ค๋ง์ Java์์, ์ค์ ์คํ์ JNI๋ฅผ ํตํด ์ปค๋์์ ์คํ๋๋ค. Java์ ์ค๋ ๋ ๋ชจ๋ธ์ ๋์ํํ๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
Heap์ ์กด์ฌํ๋ ๋ง์ ULT ์ค ํ๋๊ฐ JVM์ ์ค์ผ์ค๋ง์ ๋ฐ๋ผ KLT์ ๋งคํ๋์ด ์คํํ๋ ํํ๊ฐ ๊ธฐ์กด์ Java ์ค๋ ๋ ๋ชจ๋ธ์ด๋ค.
virtualthread">Virtual Thread
๊ธฐ์กด์ Java ์ค๋ ๋๋ฅผ ์์๋ณด์์ผ๋ ์ด์ JDK 21์ ์๋ก ๋์ ๋ Virtual Thread๋ฅผ ์์๋ณด์.
Virtual Thread concepts
Virtual Thread๋ ๊ธฐ์กด KLT(1) : ULT(1)์ ๊ตฌ์กฐ๊ฐ ์๋ KLT(1) : ULT(1) : Virtual Thread(N)์ ๊ตฌ์กฐ๋ก ์ฌ์ฉ๋๋ค. KLT์ Virtual Thread ์ฌ์ด์ ULT๋ ํ๋ซํผ ์ค๋ ๋๋ผ๊ณ ํ๋ค.
์ ๊ทธ๋ฆผ๊ณผ ๊ฐ์ด Heap์ ์๋ง์ Virtual Thread๋ฅผ ํ ๋นํด๋๊ณ , ํ๋ซํผ ์ค๋ ๋์ ๋์ Virtual Thread๋ฅผ ๋ง์ดํธ/์ธ๋ง์ดํธํ์ฌ ์ปจํ ์คํธ ์ค์์นญ์ ์ํํ๋ค. ๋ฐ๋ผ์ ์ปจํ ์คํธ ์ค์์นญ ๋น์ฉ์ด ์์์ง ์ ๋ฐ์ ์๋ค.
์ค๋ ๋์ ํฌ๊ธฐ์ ์ปจํ ์คํธ ์ค์์นญ ๋น์ฉ์ด ๋ง์ด ๊ฐ์ํ ๋ชจ๋ธ์ด๊ธฐ ๋๋ฌธ์ Spring MVC/Tomcat ๋ฑ์ ๋ชจ๋ธ์ด Netty/WebFlux์ ๋นํด ๊ฐ์ง ๋จ์ ์ด ๋ง์ด ํฌ์๋์๋ค.
Virtual Thread states
Virtual Thread์๋ 9๊ฐ์ ์ํ๊ฐ ์๋ค.
(
openjdk/jdk21
-/src/java.base/share/classes/java/lang/
VirtualThread.java
)
/**
* Virtual thread state and transitions:
*
* NEW -> STARTED // Thread.start
* STARTED -> TERMINATED // failed to start
* STARTED -> RUNNING // first run
*
* RUNNING -> PARKING // Thread attempts to park
* PARKING -> PARKED // cont.yield successful, thread is parked
* PARKING -> PINNED // cont.yield failed, thread is pinned
*
* PARKED -> RUNNABLE // unpark or interrupted
* PINNED -> RUNNABLE // unpark or interrupted
*
* RUNNABLE -> RUNNING // continue execution
*
* RUNNING -> YIELDING // Thread.yield
* YIELDING -> RUNNABLE // yield successful
* YIELDING -> RUNNING // yield failed
*
* RUNNING -> TERMINATED // done
*/
private static final int NEW = 0;
private static final int STARTED = 1;
private static final int RUNNABLE = 2; // runnable-unmounted
private static final int RUNNING = 3; // runnable-mounted
private static final int PARKING = 4;
private static final int PARKED = 5; // unmounted
private static final int PINNED = 6; // mounted
private static final int YIELDING = 7; // Thread.yield
private static final int TERMINATED = 99; // final state
๋ค์๊ณผ ๊ฐ์ด Virtual Thread์ ์ํ์ ๋ฐ๋ผ ํ๋ซํผ ์ค๋ ๋์ ๋ง์ดํธ/์ธ๋ง์ดํธํด ์คํ์ ๊ด๋ฆฌํ๋ค.
ํ๋ซํผ ์ค๋ ๋์ ์ธ๋ง์ดํธ/๋ง์ดํธํ ๋์๋ park/unpark ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ค.
(
openjdk/jdk21
-/src/java.base/share/classes/java/lang/
BaseVirtualThread.java
)
sealed abstract class BaseVirtualThread extends Thread
permits VirtualThread, ThreadBuilders.BoundVirtualThread {
/**
* Initializes a virtual Thread.
*
* <span class="hljs-doctag">@param name thread name, can be null
* <span class="hljs-doctag">@param characteristics thread characteristics
* <span class="hljs-doctag">@param bound true when bound to an OS thread
*/
BaseVirtualThread(String name, int characteristics, boolean bound) {
super(name, characteristics, bound);
}
/**
* Parks the current virtual thread until the parking permit is available or
* the thread is interrupted.
*
* The behavior of this method when the current thread is not this thread
* is not defined.
*/
abstract void park();
/**
* Parks current virtual thread up to the given waiting time until the parking
* permit is available or the thread is interrupted.
*
* The behavior of this method when the current thread is not this thread
* is not defined.
*/
abstract void parkNanos(long nanos);
/**
* Makes available the parking permit to the given this virtual thread.
*/
abstract void unpark();
}
์ ์ํ ๊ทธ๋ฆผ์ฒ๋ผ Virtual Thread์ state
๋ฅผ ๋ณ๊ฒฝ์์ผ๊ฐ๋ฉฐ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ค.
(
openjdk/jdk21
-/src/java.base/share/classes/java/lang/
VirtualThread.java
)
@Override
void park() {
/* ... ์๋ต ... */
// park on the carrier thread when pinned
if (!yielded) {
parkOnCarrierThread(false, 0);
}
}
private void parkOnCarrierThread(boolean timed, long nanos) {
assert state() == RUNNING;
/* ... ์๋ต ... */
setState(PINNED); // RUNNING -> PINNED ๋ก ์ ํ
/* ... ์๋ต ... */
}
ํ๋ซํผ ์ค๋ ๋์ ๋ง์ดํธํ์ฌ ์คํํ๋ unpark
๋ฉ์๋๋ฅผ ๋ณด์.
(
openjdk/jdk21
-/src/java.base/share/classes/java/lang/
VirtualThread.java
)
void unpark() {
/* ... ์๋ต ... */
if (s == PARKED & compareAndSetState(PARKED, RUNNABLE)) {
if (currentThread instanceof VirtualThread vthread) {
vthread.switchToCarrierThread();
try {
submitRunContinuation();
} finally {
switchToVirtualThread(vthread);
}
} else {
submitRunContinuation();
}
}
/* ... ์๋ต ... */
}
private void submitRunContinuation() {
try {
scheduler.execute(runContinuation);
} catch (RejectedExecutionException ree) {
submitFailed(ree);
throw ree;
}
}
๋ณด๋ค์ํผ scheduler
๋ก ์ค์ ์คํ์ ๋๊ธฐ๋ฉฐ, scheduler
๋ ForkJoinPool
์ด๋ค.
(
openjdk/jdk21
-/src/java.base/share/classes/java/lang/
VirtualThread.java
)
private static ForkJoinPool createDefaultScheduler() {
ForkJoinWorkerThreadFactory factory = pool -> {
PrivilegedAction<ForkJoinWorkerThread> pa = () -> new CarrierThread(pool);
return AccessController.doPrivileged(pa);
};
/* ... ์๋ต ... */
}
Virtual Thread๋ ํ๋ซํผ ์ค๋ ๋๋ฅผ ์ฐธ์กฐํ๊ณ ์์ผ๋ฉฐ ์ด๋ carrierThread
๋ผ๊ณ ํ๋ค.
// carrier thread when mounted, accessed by VM
private volatile Thread carrierThread;
์ฆ, JVM์ด ์ง์ ์ ๊ทผํ๋ ์ค๋ ๋๋ ํ๋ซํผ ์ค๋ ๋์ด๋ฉฐ, ํ๋ซํผ ์ค๋ ๋์ ๋ง์ดํธํ์ฌ ์คํํ๋ ๊ณผ์ ์ carrierThread
์ ์คํ ๋์ Virtual Thread๋ฅผ ํ ๋นํ๋ ๋ฐฉ์์ด๋ค.
private void mount() {
/* ... ์๋ต ... */
carrier.setCurrentThread(this); // -> ํ๋ซํผ ์ค๋ ๋์ ์คํํ Virtual Thread ํ ๋น
/* ... ์๋ต ... */
}
private void unmount() {
Thread carrier = this.carrierThread;
carrier.setCurrentThread(carrier);
synchronized (interruptLock) {
setCarrierThread(null); // -> Virtual Thread์์ Virtual Thread ์ ๊ฑฐ
}
carrier.clearInterrupt();
}
Virtual Thread๋ ํ๋ซํผ ์ค๋ ๋๋ฅผ ์ฐธ์กฐํ๊ณ ์์ผ๋ฉฐ ์ค์ ์คํ ์์๋ ํ๋ซํผ ์ค๋ ๋์ ๋ง์ดํธ๋์ด ForkJoinPool
์ ํ์ ๋ค์ด๊ฐ ์ค์ผ์ค๋ง๋๋ค.
private <T> ForkJoinTask<T> poolSubmit(boolean signalIfEmpty,
ForkJoinTask<T> task) {
WorkQueue q; Thread t; ForkJoinWorkerThread wt;
U.storeStoreFence(); // ensure safely publishable
if (task == null) throw new NullPointerException();
if (((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) &
(wt = (ForkJoinWorkerThread)t).pool == this)
q = wt.workQueue;
else {
task.markPoolSubmission();
q = submissionQueue(true);
}
q.push(task, this, signalIfEmpty);
return task;
}
Virtual Thread pinning
Virtual Thread์ ์ฅ์ ์, JVM์ด ์์ฒด์ ์ผ๋ก Virtual Thread๋ฅผ ์ค์ผ์ค๋งํ๊ณ ์ปจํ ์คํธ ์ค์์นญ ๋น์ฉ์ด ์ค์ด๋ค์ด ํจ์จ์ ์ผ๋ก ์ด์ํ ์ ์๋ค๋ ๊ฒ์ด๋ค. ํ์ง๋ง Virtual Thread๊ฐ ํ๋ซํผ ์ค๋ ๋์ ๊ณ ์ ๋์ด ์ฅ์ ์ ํ์ฉํ ์ ์๋ ๊ฒฝ์ฐ๊ฐ ์๋ค. Virtual Thread ๋ด์์ synchronized block์ ์ฌ์ฉํ๊ฑฐ๋, JNI๋ฅผ ํตํด ๋ค์ดํฐ๋ธ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ๋ค.
Virtual Thread๋ Spring Boot 3.2.x์์ ๊ณต์์ ์ผ๋ก ์ง์ํ์ง๋ง 2.x์์๋ ๋ณ๋๋ก ์ค์ ํด์ ์ฌ์ฉํ ์ ์๋ค
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
๋ค๋ง, ๊ณต์ ๋ธ๋ก๊ทธ์ ๋ฐ๋ฅด๋ฉด Spring ๋ก์ง ๋ด์ ๋ง์ synchronized
๊ฐ ์์ด ํจ์จ์ด ์ข์ง ์๋ค.
์ค์ ๋ก Spring Boot 2.7.17์์ Virtual Thread๋ฅผ ์ฌ์ฉํ๋๋ก ์ค์ ํ๊ณ -Djdk.tracePinnedThreads=short
์ต์
๊ณผ ํจ๊ป ๊ตฌ๋ํ ํ synchronized
๋ฅผ ์ฌ์ฉํ๋ ์ปจํธ๋กค๋ฌ๋ฅผ ํธ์ถํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋ก๊ทธ๋ฅผ ๋ง์ด ๋ณผ ์ ์๋ค.
@GetMapping("/test")
@Operation(summary = "ํ
์คํธ", description = "ํ
์คํธ")
public String test() throws Exception {
synchronized (this){
Thread.sleep(1000l);
log.info("HELLO");
}
return "OK";
}
Thread[#185,ForkJoinPool-1-worker-1,5,CarrierThreads]
com.example.test.TestController.test(TestController.java:22) <== monitors:1
๋ํ Spring ๊ตฌ๋ ์ ๋ค์๊ณผ ๊ฐ์ ๋ก๊ทธ๋ ๋ณผ ์ ์๋ค.
Thread[#184,ForkJoinPool-1-worker-2,5,CarrierThreads]
com.mysql.cj.protocol.ReadAheadInputStream.read(ReadAheadInputStream.java:180) <== monitors:1
com.mysql.cj.jdbc.ConnectionImpl.commit(ConnectionImpl.java:791) <== monitors:1
MySQL ํจํค์ง์ ์ฌ์ฉ๋ synchronized
๊ฐ pinning์ ์ ๋ฐํ๊ณ ์๋ ๊ฒ์ด๋ค.
๋ฐ๋ผ์ Spring์ synchronized
๋ฅผ ReentrantLock
์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์
ํ๋ ๋ฐฉํฅ์ผ๋ก ๊ฐ๊ณ ์๋ค.
๊ทธ ๋ฐ์๋ ๋ง์ ์ง์์์ Virtual Thread๋ฅผ ์ง์ํ๊ธฐ ์ํด synchronized
์์ ReentrantLock
์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์
์ด ์งํ๋๊ณ ์๋ค.
synchronized
๊ฐ ๋ง์ด ๋จ์์๋ Spring Boot 2.x์์๋ Virtual Thread๋ฅผ ์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ ์ฌ๋ฌ ์์กด ๋ชจ๋์ ๋ง์ด๊ทธ๋ ์ด์
์ด ์ ํ๋์ด์ผ ํ ๊ฒ ๊ฐ๋ค. ์์ผ๋ก ๋ฏธ๋ Java ๋ฒ์ ์์๋ synchronized
๋ ์ ์ ์ฌ๋ผ์ง ๊ฒ์ผ๋ก ์์ํ๋ค.
Virtual Thread blocking
๊ธฐ์กด Java ์ค๋ ๋๋ sleep ์คํ ์ blocking ์ํ๊ฐ ๋๋ฉฐ ๋ค๋ฅธ ์ค๋ ๋์ ์ปจํ ์คํธ ์ค์์นญ์ ํ๋ค. Virtual Thread์ sleep์ ์ดํด๋ณด์.
public static void sleep(long millis) throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
long nanos = MILLISECONDS.toNanos(millis);
ThreadSleepEvent event = beforeSleep(nanos);
try {
if (currentThread() instanceof VirtualThread vthread) {
vthread.sleepNanos(nanos);
} else {
sleep0(nanos);
}
} finally {
afterSleep(event);
}
}
๊ธฐ์กด ์ค๋ ๋์ ๊ฒฝ์ฐ sleep0 JNI ํธ์ถ๋ก KLT์ ํจ๊ป block ์ํ๋ก ๋ณ๊ฒฝ๋๊ณ Virtual Thread์ ๊ฒฝ์ฐ ๋ค๋ฅธ ๋์์ ํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
void sleepNanos(long nanos) throws InterruptedException {
/* ... ์๋ต ... */
parkNanos(remainingNanos);
/* ... ์๋ต ... */
}
@Override
void parkNanos(long nanos) {
/* ... ์๋ต ... */
boolean yielded = false;
Future<?> unparker = scheduleUnpark(this::unpark, nanos);
setState(PARKING);
try {
yielded = yieldContinuation(); // may throw
/* ... ์๋ต ... */
} catch(/* ... ์๋ต ... */) {
}
}
private boolean yieldContinuation() {
// unmount
notifyJvmtiUnmount(/*hide*/true);
unmount();
try {
return Continuation.yield(VTHREAD_SCOPE);
} finally {
// re-mount
mount();
notifyJvmtiMount(/*hide*/false);
}
}
์ค๋ ๋๋ฅผ ์ธ๋ง์ดํธ/parkํ๊ณ ๋ค์ ๋ง์ดํธ/unparkํ๋ ๊ฒ์ Future๋ก ๋๋ฆฌ๋ ๊ฒ์ ์ ์ ์๋ค. ์ฆ, ๋ช ์์ ์ธ KLT์ sleep/block์ ์ํํ์ง ์๋๋ค.
Spring MVC Tomcat ํ์์ ํ ์คํธ๋ฅผ ํด๋ณด์. Virtual Thread๋ฅผ ์ฌ์ฉํ์ง ์๋ Tomcat์ threads๋ฅผ 1๋ก ์ค์ ํ์ฌ ์ปค๋ ์ค๋ ๋๋ฅผ ํ๋๋ง ์ฌ์ฉํ๊ฒ ํ๊ณ , Virtual Thread์์๋ ์ปค๋ ์ค๋ ๋๋ฅผ ํ๋๋ง ์ฌ์ฉํ๊ฒ ํ์ฌ ์ฒ๋ฆฌ๋์ ๋น๊ตํด ๋ณด๊ฒ ๋ค. ๋ํ ํธ์ถ์ 100๊ฐ์ ์์ฒญ์ ๋์์ ๋ณด๋ด ๋ณด๊ฒ ๋ค.
๋ค์ ์ปจํธ๋กค๋ฌ๋ฅผ ํธ์ถํ๋ค.
@GetMapping("/test")
@Operation(summary = "ํ
์คํธ", description = "ํ
์คํธ")
public String test() throws Exception {
Thread.sleep(1000l);
log.info("{}", Thread.currentThread());
return "OK";
}
Tomcat์ ๋ค์ ์ค์ ์ผ๋ก ์ค๋ ๋๋ฅผ ์ ํํ๋ค.
server:
tomcat:
threads:
max: 1
Virtual Thread๋ ๊ฐ์ด๋์ ๋ฐ๋ผ ๋ค์ ํ๊ฒฝ๋ณ์๋ฅผ ํตํด ์ค๋ ๋๋ฅผ ์ ํํ๋ค.
Virtual Thread๋ฅผ ์ฌ์ฉํ์ง ์์ ํ๊ฒฝ์์๋ 100๊ฐ์ ํธ์ถ์ด ๋์์ ๋ฐ์ํ์ผ๋, Tomcat ์ค๋ ๋๊ฐ 1์ด๋ฏ๋ก ํธ์ถ ์ฒ๋ฆฌ์ ์ต๋ ์ ์ฒ๋ฆฌ ์๊ฐ์ด ๊ฑธ๋ฆฌ๊ณ 1TPS์ ์ฒ๋ฆฌ๋์ ๋์ง ๋ชปํ๋ค. ์ฆ, ๋์์ฑ์ด ๊ฑฐ์ ์๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
Name | # reqs | # fails | Avg | Min | Max | Median | req/s | failures/s |
---|---|---|---|---|---|---|---|---|
GET /test | 23 | 0(0.00%) | 11986 | 1021 | 22943 | 12000 | 0.99 | 0.00 |
Virtual Thread๋ฅผ ์ฌ์ฉํ ํ๊ฒฝ์์๋ ๋์ TPS ์ฒ๋ฆฌ๋์ ๋ณด์ธ๋ค. 100๊ฐ์ ํธ์ถ์ด ๋์์ ๋ฐ์ํ์ผ๋, non-blocking ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌ๋์ด ์ต๋ ์ฒ๋ฆฌ ์๊ฐ ๋ํ 1000l ์ ๋๋ค. ๋ํ ๋ก๊ทธ์์ ์ปค๋ ์ค๋ ๋๋ ํ๋๋ง ์ฌ์ฉํ๋ ๊ฒ์ ์ ์ ์๋ค.
Name | # reqs | # fails | Avg | Min | Max | Median | req/s | failures/s |
---|---|---|---|---|---|---|---|---|
GET /test | 928 | 0(0.00%) | 1005 | 1001 | 1031 | 1001 | 89.19 | 0.00 |
2024-02-05 13:17:26.329 INFO 70581 --- [ ] VirtualThread[#312]/runnable@ForkJoinPool-1-worker-1
2024-02-05 13:17:26.336 INFO 70581 --- [ ] VirtualThread[#313]/runnable@ForkJoinPool-1-worker-1
2024-02-05 13:17:26.339 INFO 70581 --- [ ] VirtualThread[#314]/runnable@ForkJoinPool-1-worker-1
2024-02-05 13:17:26.349 INFO 70581 --- [ ] VirtualThread[#315]/runnable@ForkJoinPool-1-worker-1
๋ฐ๋ผ์ Tomcat, Spring MVC ํ์์๋ Netty/WebFlux์ ์ฒ๋ฆฌ ๋ฐฉ์๊ณผ ํจ์จ์ด ๊ฐ์ผ๋ฉฐ, ๋คํธ์ํฌ I/O์ฒ๋ผ CPU๋ฅผ ์ฌ์ฉํ์ง ์๋ ์ค๋ ๋ blocking ํ๊ฒฝ์์ ์ฌ์ฉํ๋ฉด ์ข์ ํจ์จ์ ๋ณด์ฌ์ค ์ ์๋ค๊ณ ํ๋จํ ์ ์๋ค.
๋ง์น๋ฉฐ
CPU intensive ํ๊ฒฝ์ด ์๋, ๋คํธ์ํฌ I/O๊ฐ ๋ค์ ๋ฐ์ํ๋ ์น์๋ฒ ํ๊ฒฝ์์๋ ํ๋์ ํธ์ถ์ ํ๋์ ์ค๋ ๋๋ฅผ ์ ์ ํ๋ ๊ธฐ์กด Spring MVC/Tomcat ๋ชจ๋ธ์ ํฐ ๋ถ๋ด์ผ๋ก ์์ฉํ๊ณ , non-blocking single thread ๋ชจ๋ธ์ธ Netty/WebFlux ๋ชจ๋ธ์ด ๊ทธ ๋จ์ ์ ํด๊ฒฐํ๋ฉฐ ๋ถ์ํ๋ค. ํ์ง๋ง ํ์ต์ด ์ด๋ ต๊ณ , ์๋ จ๋๊ฐ ๋ถ์กฑํด block์ ํ ๋ฒ์ด๋ผ๋ ์๋ชป ์ฌ์ฉํ๋ ์๊ฐ ์ ์ฒด ์๋น์ค๊ฐ ๋ง๊ฐ์ง๊ธฐ ๋๋ฌธ์ ์ฝ๊ฒ ๋์ ํ๊ธด ์ฝ์ง ์๋ค๊ณ ์๊ฐํ๋ค. ๋ ๊ฑฐ์ ์๋น์ค์ ๊ฒฝ์ฐ webflux๋ก ๋ง์ด๊ทธ๋ ์ด์ ํ๊ธฐ๋ ์ด๋ ค์ธ ๊ฒ์ด๋ค.
Virtual Thread์ ๋ฑ์ฅ์ non-blocking single thread ๋ชจ๋ธ์ ์ฌ์ฉํ์ง ์์๋ ๋๋ค๊ณ ๋งํ๊ณ ์๋ค. ์ค์ ๋ก CPU intensive ํ๊ฒฝ์ด ์๋๋ผ๋ฉด non-blocking single thread ๋ชจ๋ธ๋งํผ์ด๋ ํจ์จ์ ์ ๋ด๊ณ ์๋ค.
์ถํ ๋ง์ Java ์ง์์์ synchronized๋ฅผ ์ ๊ฑฐํ๋ ๋ฑ, Virtual Thread๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํ ์ค๋น๊ฐ ๋๋ค๋ฉด Java ์ง์์ non-blocking single thread ๋ชจ๋ธ์ ์๋ฆฌ์ Virtual Thread๊ฐ ๋ค์ด๊ฐ์ง๋ ๋ชจ๋ฅด๊ฒ ๋ค.