Skip to content

Java 24: Virtual threads no longer pinned by synchronized (JEP 491) - #121

Merged
mperor merged 2 commits into
masterfrom
DAILY/97-virtual-threads-no-pinning
Nov 6, 2026
Merged

mperor merged 2 commits into
masterfrom
DAILY/97-virtual-threads-no-pinning

Conversation

@mperor

@mperor mperor commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

What you'll learn

  • Why many virtual threads blocking inside synchronized used to exhaust carrier threads, and how JEP 491 fixes that in JDK 24+
  • The one pinning case JEP 491 does not fix: a native (JNI) frame still pins the carrier
  • That a thread blocked entering a contended synchronized block ignores interrupt(), unlike ReentrantLock#lockInterruptibly

Why it matters

Picking synchronized vs. java.util.concurrent locks for virtual-thread code used to be a real capacity-planning decision; JEP 491 removes most of that cost, but interruptibility, tryLock, fairness and multiple Conditions are still reasons to reach for ReentrantLock.

Note: this sandbox's network policy blocks every JDK 24/25 distribution host, so ./gradlew could not run locally. I validated the new tests by compiling and running them standalone against a local JDK 21 + JUnit 6.1: they compile cleanly, and testManyVirtualThreadsBlockingInsideSynchronizedDoNotPinCarriers fails on JDK 21 exactly as predicted (elapsed time matches the pinned-carrier estimate almost exactly, confirming real pinning). CI has since run on JDK 25 and is green (306 tests, +3, 0 failures), confirming the fix actually works there.

Read more

JEP 491: Synchronize Virtual Threads without Pinning

Quiz

Before JDK 24, 1000 virtual threads each enter their own (uncontended) synchronized block and call Thread.sleep(80) inside it, on a machine with 8 carrier threads. Roughly how long does the whole batch take, and why?

Answer

Roughly 1000 / 8 * 80 ms ≈ 10 seconds. Blocking inside synchronized pinned each virtual thread to its carrier thread, so only 8 could be "sleeping" at once; the rest queued up waiting for a free carrier. On JDK 24+ (JEP 491) the carrier is released instead, so all 1000 run close to concurrently and the batch finishes in about 80 ms.

Closes #97

@mperor mperor added the daily Queued for the daily learning PR label Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

Test Results

306 tests  +3   303 ✅ +3   5s ⏱️ -1s
 84 suites ±0     3 💤 ±0 
 84 files   ±0     0 ❌ ±0 

Results for commit a8298de. ± Comparison against base commit a9c068a.

This pull request skips 1 and un-skips 1 tests.
pl.mperor.lab.java.design.pattern.creational.singleton.SingletonTest ‑ testThreadSafe()
pl.mperor.lab.java.design.pattern.creational.singleton.SingletonTest ‑ testSerializationSafe()

@mperor
mperor marked this pull request as ready for review November 5, 2026 07:14
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

@mperor ⏰ This daily topic is still waiting for your review.

@mperor
mperor merged commit 40465e6 into master Nov 6, 2026
1 check passed
@mperor
mperor deleted the DAILY/97-virtual-threads-no-pinning branch November 6, 2026 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

daily Queued for the daily learning PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Java 24: Virtual threads no longer pinned by synchronized (JEP 491)

2 participants