Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Java 21/25 Virtual Threads 소개

Java 21/25 Virtual Threads 소개

Java21, 25의 Virtual Threads 소개

- Virtual Threads 개요
- 적용방법
- 성능 비교

Avatar for Sunghyouk Bae (Debop)

Sunghyouk Bae (Debop)

March 22, 2026
Tweet

More Decks by Sunghyouk Bae (Debop)

Other Decks in Programming

Transcript

  1. Agenda • Virtual Threads ѐਃ • Rules & Best practices

    • Spring Boot with Virtual Threads • Legacy Code ߸҃ Generated by ChatGPT
  2. Virtual Threads (Loom project) ۆ • Virtual Threads ౠ૚ •

    Lightweight Threads ( Not Fiber, Not Coroutines ) • Virtual Thread Per Request • Non-Blocking ૑ਗ • ӝઓ Platform Thread৬ ഐജؼ ࣻ ੓ب۾ ೠ׮ • Legacy Code੄ ߸ചহ੉ ࢎਊ оמ೧ঠ ೠ׮ • Coroutines, Reactive Programming ୊ۢ ѐߊ ߑध ੹୓ܳ ੹ജ೧ঠ ೞח Ѫ੉ ইפ׮ • Virtual threads are syntactic sugar for reactive programming • ࠺زӝ ௏٘ܳ sequential code ۽ ಴അ оמ — Readable Code
  3. Quiz Platform Threads vs Virtual Threads - Blocking ? @Test

    fun `Platform Thread ۽ Blocking ௏٘ प೯ೞӝ`() { val threads = List(THREAD_SIZE) { Thread.ofPlatform().start { Thread.sleep(1000) log.debug { "Platform Thread $it" } } } threads.forEach { it.join() } } @Test fun `Virtual Thread ۽ Blocking ௏٘ प೯ೞӝ`() { val threads = List(THREAD_SIZE) { Thread.ofVirtual().start { Thread.sleep(1000) log.debug { "Virtual Thread $it" } } } threads.forEach { it.join() } } 100,000 * 1 sec / core + logging Under 3 seconds
  4. Spring Boot IO ࠗೞ పझ౟ Spring Boot 2025: Parallel Database

    Queries with Virtual Threads ജ҃: Spring Boot 3.x, PostgreSQL, 100 زद োѾ ߑध Throughput Latency Platform Threads (MVC) 884 req/s 448 ms Virtual Threads (MVC) 967 req/s 391 ms Improvement 14% -28.8% ജ҃: ߽۳ ௪ܻ दաܻয় (ৈ۞ ௪ܻܳ زद प೯) ߑध Throughput Latency Platform Threads (ࣽର) ~220 req/s 115 ms Virtual Threads (߽۳) ~2,600 req/s 43 ms Improvement x11.8 -62%
  5. JVM Platform Threads ੘ز ߑध Application Thread Pool Platform Thread

    Platform Thread Platform Thread Platform Thread OS OS Thread OS Thread OS Thread OS Thread
  6. JVM Virtual Threads ੘ز ߑध Application Carrier Thread Carrier Thread

    Platform Thread Carrier Thread OS OS Thread OS Thread OS Thread OS Thread Virtual Thread Scheduler Virtual Thread Virtual Thread Virtual Thread Virtual Thread Virtual Thread
  7. JVM Virtual Threads ੘ز ߑध Application Carrier Thread Carrier Thread

    Platform Thread Carrier Thread OS OS Thread OS Thread OS Thread OS Thread Virtual Thread Scheduler Virtual Thread Virtual Thread Blocking
  8. Virtual Threads ࢿמ ࠺Ү Platform Thread Virtual Thread Coroutines Startup

    time > 1,000 us 1~10 us 1~10 us Metadata Size ~ 2kb 200~300 byte Memory ޷ܻ ೡ׼ػ Stack ࢎਊ (1MB) ೙ਃ द Heap ࢎਊ (~1,400 bytes) ೙ਃ द Heap ࢎਊ (~300 bytes) Context switching cost 1 ~ 10 us (ழօ৔৉) ~ 2 us ~ 1 us Numbers < 5,000 Millions Millions Best cases CPU Bounded Per Request Legacy Code High-concurrent code Event based Systems Structured concurrency and cancellation
  9. Rule 1 - Create Threads by Builder val builder =

    Thread.ofPlatform() .daemon(false) .priority(10) .stackSize(1024) .name("platform-thread") .inheritInheritableThreadLocals(false) .uncaughtExceptionHandler { thread, ex -> print("Thread[$thread] failed with exception: $ex") } val thread = builder.unstarted { print(“Unstarted Platform Thread") } val builder = Thread.ofVirtual() .name("virtual-thread") .inheritInheritableThreadLocals(false) .uncaughtExceptionHandler { thread, ex -> log.error(ex) { "Thread[$thread] failed with exception." } } val thread = builder.unstarted { println("Unstarted Virtual Thread") }
  10. Rule 2 - Do Not use CompletableFuture For Legacy synchronous

    code fun `زӝ ௏٘ܳ CompletableFuture ۽ प೯ೞӝ`() { val startMs = System.currentTimeMillis() CompletableFuture .supplyAsync { readPriceInEur() } .thenCombine( CompletableFuture.supplyAsync { readExchangeRateEurToUsd() }) { price, rate -> price * rate } .thenCompose { amount -> CompletableFuture.supplyAsync { amount * (1.0F + readTax(amount)) } } .whenComplete { grossAmountInUsd, error -> if (error == null) { grossAmountInUsd.toInt() shouldBeEqualTo 108 } else { fail(error) } } .get() // য૰ٚ ৈӝࢲ Blocking ػ׮ (Non-Blocking ੉ ইפ׮) val durationMs = System.currentTimeMillis() - startMs durationMs shouldBeInRange 800L..900L }
  11. Rule 2 - Do use Virtual Threads For Legacy synchronous

    code fun `زӝ ௏٘ܳ Virtual Thread ۽ प೯ೞӝ`() { Executors.newVirtualThreadPerTaskExecutor().use { executor -> val startMs = System.currentTimeMillis() val priceInEur = executor.submit<Int> { readPriceInEur() } val exchangeRateEuroToUsd = executor.submit<Float> { readExchangeRateEurToUsd() } val netAmountInUsd = priceInEur.get() * exchangeRateEuroToUsd.get() val tax = executor.submit<Float> { readTax(netAmountInUsd) } val grossAmountInUsd = netAmountInUsd * (1.0F + tax.get()) grossAmountInUsd.toInt() shouldBeEqualTo 108 val durationMs = System.currentTimeMillis() - startMs durationMs shouldBeInRange 800L..900L } }
  12. Cf. Use Coroutines Change Legacy code to suspended fun `Suspend

    ೣࣻܳ Coroutines ജ҃ীࢲ प೯ೞӝ`() = runSuspendTest(Dispatchers.Default) { val startMs = System.currentTimeMillis() val priceInEur = async { readPriceInEurAwait() } val exchangeRateEuroToUsd = async { readExchangeRateEurToUsdAwait() } val netAmountInUsd = priceInEur.await() * exchangeRateEuroToUsd.await() val tax = async { readTax(netAmountInUsd) } val grossAmountInUsd = netAmountInUsd * (1.0F + tax.await()) grossAmountInUsd.toInt() shouldBeEqualTo 108 val durationMs = System.currentTimeMillis() - startMs durationMs shouldBeInRange 800L..900L }
  13. Rule 3 - Do not use Thread Pool Virtual Threads

    ࢎਊ दীח Thread Pool ਸ ࢎਊೞ૑ ݃ۄ. Thread ࣻܳ ઁೠೡ ੉ਬо হ׮ fun `࠺୶ୌ - ThreadPool ীࢲ Virtual Thread ࢤࢿೞӝ`() { Executors.newCachedThreadPool(Thread.ofVirtual().factory()).use { executor -> executor.submit { Thread.sleep(1000) log.debug { "1 run ${Thread.currentThread()}" } } executor.submit { Thread.sleep(1000) log.debug { "2 run ${Thread.currentThread()}" } } } }
  14. Rule 3 - Do use newThreadPerTaskExecutor Virtual Threads ח Million

    ױਤ۽ ࢤࢿೞח Ѫ੉ ੢੼ @Test fun `୶ୌ - ThreadPerTaskExecutor ۽ Virtual Thread ࢤࢿೞӝ`() { // Executors.newVirtualThreadPerTaskExecutor Executors.newThreadPerTaskExecutor(Thread.ofVirtual().factory()).use { executor -> executor.submit { Thread.sleep(1000) log.debug { "1 run ${Thread.currentThread()}" } } executor.submit { Thread.sleep(1000) log.debug { "2 run ${Thread.currentThread()}" } } } }
  15. Rule 4 - Do not use FixedThreadPool for concurrency ӝઓ

    زӝച ӝߨ਷ উ ాೠ׮ fun `࠺୶ୌ - FixedThreadPoolਸ ࢎਊೞৈ زदࢿ ઁযೞӝ`() { val results = ConcurrentLinkedQueue<Future<String>>() Executors.newFixedThreadPool(8, Thread.ofVirtual().factory()).use { executor -> val futures = List(100) { index -> executor.submit { log.debug { "Start run task[$index]" } val result = executor.submit<String> { sharedResource() } log.debug { "Finish run task[$index]" } results.add(result) } } futures.forEach { it.get() } results.size shouldBeEqualTo 100 } }
  16. Rule 4 - Do use Semaphore for Concurrency Local /

    Distributed Semaphore (Redisson/Lettuce Semaphore) fun `୶ୌ - Semaphoreܳ ੉ਊೞৈ زदࢿ ઁযೞӝ`() { val results = ConcurrentLinkedQueue<String>() Executors.newVirtualThreadPerTaskExecutor().use { executor -> val futures = List(100) { index -> executor.submit { log.debug { "Start run task[$index]" } val result = useSemaphoreToLimitConcurrency() log.debug { "Finish run task[$index]" } results.add(result) } } futures.forEach { it.get() } results.size shouldBeEqualTo 100 } } private val semaphore = Semaphore(8) private fun useSemaphoreToLimitConcurrency(): String { semaphore.acquire() return try { sharedResource() } finally { semaphore.release() } }
  17. Rule 5 - Do not use ThreadLocal ThreadLocal ਷ Carrier

    Threadী ઙࣘػ׮ private val threadLocal = InheritableThreadLocal<String>() @Test fun `비추천 - ThreadLocal 변수를 사용하기`() { threadLocal.set("zero") threadLocal.get() shouldBeEqualTo "zero" threadLocal.set("one") threadLocal.get() shouldBeEqualTo "one" val childThread = Thread { threadLocal.get() shouldBeEqualTo "one" } childThread.start() childThread.join() threadLocal.remove() threadLocal.get().shouldBeNull() }
  18. Rule 5 - Do use ScopedValue private val scopedValue =

    ScopedValue.newInstance<String>() @EnabledOnJre(JRE.JAVA_21, JRE.JAVA_25) @Test fun `추천 - ScopedValue 사용하기`() { ScopedValue.where(scopedValue, "zero").run { scopedValue.get() shouldBeEqualTo "zero" ScopedValue.where(scopedValue, "one").run { scopedValue.get() shouldBeEqualTo "one" } scopedValue.get() shouldBeEqualTo "zero" try { structuredTaskScopeAll { scope -> // Scope 안에서 sub task를 생성합니다. scope.fork { scopedValue.get() shouldBeEqualTo "zero" -1 } scope.join().throwIfFailed() } } catch (e: InterruptedException) { fail(e) } } assertFailsWith<NoSuchElementException> { scopedValue.get() } }
  19. Rule 6 - Do use ReentranceLock instead of synchronized Synchronized

    ח Carrier Thread ܳ Blocking ೤פ׮ private val lock = ReentrantLock() @Test fun `୶ୌ - ܻࣗझܳ ة੼੸ਵ۽ ࢎਊೡ ٸ ReentrantLockਸ ੉ਊೞࣁਃ - Kotlin withLock ೣࣻ`() { virtualFuture { lock.withLock { exclusiveResource() } }.await() } private fun exclusiveResource(): String { Thread.sleep(100) return "result" } How to solve the pinning problem in Java virtual threads
  20. Rule 7.1 - Do Structured Concurrency StructuredTaskScope.ShutdownOnFailure (Java 21) /**

    * ࠺زӝ ௏٘ܳ ߽۳۽ प೯ೞҊ, ݽٚ ੘স੉ ࢿҕ੸ਵ۽ ৮ܐغݶ Ѿҗܳ ߈ജ೤פ׮. */ fun prepareDish(): Dish = structuredTaskScopeAll { scope -> println("prepare Dish ...") val pasta = scope.fork { Thread.sleep(100) preparePasta() } val sauce = scope.fork { Thread.sleep(200) makeSaurce() } Thread.sleep(5) scope.join().throwIfFailed() Dish(pasta.get(), sauce.get()) } inline fun <T> structuredTaskScopeAll( name: String? = null, factory: ThreadFactory = Thread.ofVirtual().factory(), block: (scope: StructuredTaskScope.ShutdownOnFailure) -> T, ): T { return StructuredTaskScope.ShutdownOnFailure(name, factory).use { scope -> block(scope) } }
  21. Rule 7.1 - Do Structured Concurrency StructuredTaskScope.ShutdownOnSuccess (Java 21) fun

    `structured task scope on success`() { // Subtask ٜ ઺ ೞաۄب ࢿҕೞݶ, աݠ૑ Subtask ٜ਷ ஂࣗೞҊ, Ѿҗܳ ߈ജ೤פ׮. // ݅ড ࢿҕೠ Ѫ੉ হ׮ݶ ExecutionException ਸ ߈ജ೤פ׮. val pasta = structuredTaskScopeFirst<Pasta> { scope -> val subtask1 = scope.fork { Thread.sleep(100) preparePasta() } val subtask2 = scope.fork { Thread.sleep(200) preparePasta() } scope.join() subtask1.state() shouldBeEqualTo StructuredTaskScope.Subtask.State.SUCCESS subtask2.state() shouldBeEqualTo StructuredTaskScope.Subtask.State.UNAVAILABLE scope.result { ExecutionException(it) } } } inline fun <T> structuredTaskScopeFirst( name: String? = null, factory: ThreadFactory = Thread.ofVirtual().factory(), block: (scope: StructuredTaskScope.ShutdownOnSuccess<T>) -> T, ): T { return StructuredTaskScope.ShutdownOnSuccess<T>(name, factory).use { scope -> block(scope) } }
  22. Rule 7.2 - Do Structured Concurrency StructuredTaskScope.Joiner.awaitAll() (Java 25) private

    val provider = Jdk25StructuredTaskScopeProvider() @Test fun `withAll success`() { val result = provider.withAll { scope -> val subTask1 = scope.fork { log.debug { "Subtask 1 실행" } 10 } val subTask2 = scope.fork { log.debug { "Subtask 2 실행" } 20 } scope.join().throwIfFailed() subTask1.get() + subTask2.get() } result shouldBeEqualTo 30 } override fun <T> withAll( name: String?, factory: ThreadFactory, block: (scope: StructuredTaskScopeAll) -> T, ): T { log.debug { "모든 subtask 가 완료될 때까지 기다립니다..." } val scope = StructuredTaskScope.open<Any?, Void>( StructuredTaskScope.Joiner.awaitAll(), configure(name, factory) ) return scope.use { block(Jdk25AllScope(it)) } }
  23. Rule 7.2 - Do Structured Concurrency StructuredTaskScope.Joiner.anySuccessfulResultOrThrow() (Java 25) @Test

    fun `withAny should return first success`() { val result = provider.withAny { scope -> scope.fork { Thread.sleep(80) log.debug { "Slow subtask starting..." } "slow" } scope.fork { Thread.sleep(10) log.debug { "Fast subtask starting..." } "fast" } scope.join().result { IllegalStateException(it) } } result shouldBeEqualTo "fast" } override fun <T> withAny( name: String?, factory: ThreadFactory, block: (scope: StructuredTaskScopeAny<T>) -> T, ): T { log.debug { "첫번째로 완료된 subtask의 결과를 반환합니다." } val scope = StructuredTaskScope.open<T, T>( StructuredTaskScope.Joiner.anySuccessfulResultOrThrow(), configure(name, factory) ) return scope.use { block(Jdk25AnyScope(it)) } }
  24. Rule 8 - Test Virtual Threads @Test fun `get all

    actors in multiple virtual threads`() { StructuredTaskScopedTester() .rounds(100) .add { transaction(db) { val actors = Actor.all().toList() actors.shouldNotBeEmpty() } } .run() } add ೣࣻী ઱য૓ ௏٘ ࠶۟ਸ rounds ࣻ ݅ఀ प೯ೠ׮
  25. Spring Boot IO ࠗೞ పझ౟ (Recap) Spring Boot 2025: Parallel

    Database Queries with Virtual Threads ജ҃: Spring Boot 3.x, PostgreSQL, 100 زद োѾ ߑध Throughput Latency Platform Threads (MVC) 884 req/s 448 ms Virtual Threads (MVC) 967 req/s 391 ms Improvement 14% -28.8% ജ҃: ߽۳ ௪ܻ दաܻয় (ৈ۞ ௪ܻܳ زद प೯) ߑध Throughput Latency Platform Threads (ࣽର) ~220 req/s 115 ms Virtual Threads (߽۳) ~2,600 req/s 43 ms Improvement x11.8 -62%
  26. ো࢑ ਬഋ߹ ౠࢿ ਃড ࢚ട ୶ୌ ࢎਬ JDBC ௪ܻ ૘઺

    Virtual Threads I/O ؀ӝ ઺ carrier ߈ժ -> ബਯ 2~5ߓ CPU bound ো࢑ Platform Threads (ForkJoin Pool) Virtual য়ߡ೻٘ ޖ੄޷, carrier ࣻ = CPU ௏য ࣻо ୭੸ +latency ജ҃ Virtual Threads ForkJoinPool ৉੹, CachedPool ݫݽܻ ਤ೷ ؀ӏݽ ߽۳ ௪ܻ Virtual Threads Thread.startVirtualThread { repo.find() } ಁఢਵ۽ Webflux ࣻળ ׳ࢿ Reactive ؀୓ ৈࠗ Virtual Threads ͌ R2DBC Ops/s ز١, ௏٘ ࠂ੟بח Virtual Threads о ഻ঁ ծ਺ Sequential style - ਬ૑ࠁࣻ ࣻਘ
  27. Spring Boot MVC with Virtual Threads @Configuration(proxyBeanMethods = false) class

    TomcatConfig { /** * Tomcat ProtocolHandler੄ executor ܳ Virtual Thread ܳ ࢎਊೞח Executorܳ ࢎਊೞب۾ ࢸ੿ */ @Bean fun protocolHandlerVirtualThreadExecutorCustomizer(): TomcatProtocolHandlerCustomizer<*> { return TomcatProtocolHandlerCustomizer<ProtocolHandler> { protocolHandler -> protocolHandler.executor = Executors.newVirtualThreadPerTaskExecutor() } } } server: tomcat: threads: max: 8000 # ӝࠄ: 200, Virtual Threadsח ݅ ױਤ ੉࢚ਵ۽ ૑੿೧ب ޖߑೞ׮ min-spare: 20 application.yml
  28. Spring Boot MVC Async Tasks // `@Async` য֢ప੉࣌੉ ੸ਊػ ݫࣗ٘ܳ

    Virtual Threaedܳ ੉ਊೞৈ ࠺زӝ۽ प೯ೞӝ ਤೠ ࢸ੿ @Configuration(proxyBeanMethods = false) @EnableAsync class AsyncConfig { @Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) fun asyncTaskExecutor(): AsyncTaskExecutor { val factory = Thread.ofVirtual().name("async-vt-exec-", 0).factory() return TaskExecutorAdapter(Executors.newThreadPerTaskExecutor(factory)) } }
  29. RestController use Virtual Threads @GetMapping("/multi") fun multipleTasks(): String { val

    taskSize = 1000 // ShutdownOnFailure - ೞաۄب पಁೞݶ ૊द ઙܐ (૓೯ ઺ੋ ׮ܲ Task ٜ਷ ઺ױػ׮) StructuredTaskScope.ShutdownOnFailure().use { scope -> repeat(taskSize) { scope.fork { Thread.sleep(1000) log.debug { "Task $it is done. (${Thread.currentThread()})" } } } scope.join().throwIfFailed() } return "Run multiple[$taskSize] tasks. (${Thread.currentThread()})" }
  30. ӝઓ दझమী Virtual Threads ੸ਊೞӝ • ؀࢚ • IO Bounded

    Operations • API ഐ୹ (ӝઓ Feign, WebClient ഐ୹ ߑध ߸҃) • Database IO ੘স (୭न JDBC Driver ࢎਊ ೙ਃ) • Ӓ ৻ IO ੘স ઺ زӝ ߑध ژח CompletableFutureܳ ࢎਊೞח Ҕ (౵ੌ ੘স, ࠙࢑ நद ١) • ઁ৻ ؀࢚ • CPU Intensive (Bounded) Operations (ஸ۩࣌ ੘স ١) • WAS Thread Pool ߸҃ • Spring Boot embedded web server(Tomcat)੄ Thread Poolਸ Virtual Threads ۽ ߸ജ
  31. Resources • Concurrent programming in Java with virtual threads •

    Java 21 Virtual Threads - Dude, Where’s My Lock? - Netflix blog • Embracing Virtual Threads - Spring blog • Virtual Thread ੄ ӝࠄ ѐ֛ ੉೧ೞӝ - Naver D2 • [Project Loom] Virtual Thread ী ࠆ(Spring)਷ ৳חо - KakaoPay • Code examples • Bluetape4k-workshop/virtualthreads
  32. Q&A