How do you use Thread.Builder (Thread.ofVirtual()) to configure virtual threads, and when would you prefer it over the one-liner?
answer
- ofVirtual() -> Thread.Builder.OfVirtual
- name(prefix, start) = auto-incrementing names
- uncaughtExceptionHandler + inheritInheritableThreadLocals
- factory() -> reusable ThreadFactory
- builder when you need config/name/handler/reference
basics
~20 sThread.ofVirtual() returns a Thread.Builder you can configure — set a name (optionally with an auto-incrementing counter), an uncaught-exception handler, thread-local inheritance — then call .start(runnable) to run it or .unstarted(runnable) to get it ready but not started. Prefer it over startVirtualThread when you need that configuration or a Thread reference before it runs.
solid answer
~40 sThread.ofVirtual() returns a Thread.Builder.OfVirtual, a fluent, reusable factory. Common settings: .name("prefix-", start) gives each built thread a name with an auto-incrementing suffix (great for logs), .name("fixed") sets a single name, .uncaughtExceptionHandler(h) installs a handler, and .inheritInheritableThreadLocals(false) opts out of inheriting inheritable thread-locals. A single configured builder can be turned into a ThreadFactory via .factory() and reused to mint many threads. From the builder you either .start(runnable) (build and start) or .unstarted(runnable) (build in NEW state, start later). Use the builder over Thread.startVirtualThread whenever you need named threads for observability, a custom exception handler, control over thread-local inheritance, a reusable ThreadFactory, or a reference to the thread before it begins. The parallel Thread.ofPlatform() builder configures platform threads the same way, so the API is symmetric.
code
java · 16 lines// Configured virtual-thread builder, reused as a ThreadFactory
Thread.Builder.OfVirtual builder = Thread.ofVirtual()
.name("req-", 0) // req-0, req-1, ...
.inheritInheritableThreadLocals(false)
.uncaughtExceptionHandler((t, e) ->
System.err.println(t.getName() + " failed: " + e));
// Start one now:
Thread t = builder.start(() -> handle(request));
// Or build it without starting, start later:
Thread later = builder.unstarted(() -> handle(other));
later.start();
// Reuse the same configuration as a factory:
java.util.concurrent.ThreadFactory factory = builder.factory();go deeper
Knows Thread.ofVirtual() gives a builder and that you can set a name before starting with .start().
Configures name (incl. prefix+counter), exception handler, and thread-local inheritance, and knows start vs unstarted and when to choose the builder.
Uses .factory() to produce a reusable ThreadFactory, understands the daemon/priority constraints on virtual threads, and weighs builder vs one-liner per use case.
Standardizes thread naming/handler conventions across a codebase for observability and integrates configured factories into the application's executor/concurrency strategy.
## What the builder is Java 21 added a unified, fluent **`Thread.Builder`** API for constructing threads, with two flavors obtained from static factory methods: - **`Thread.ofVirtual()`** → `Thread.Builder.OfVirtual` (builds **virtual** threads) - **`Thread.ofPlatform()`** → `Thread.Builder.OfPlatform` (builds **platform** threads) A *builder* is an object you configure step by step with chained method calls, then ask to produce the final object. It replaces the old grab-bag of `Thread` constructors with something readable and reusable. ## What you can configure (virtual flavor) - **`.name(String)`** — a single fixed name. A virtual thread is unnamed (empty string) by default, so naming aids logging/debugging. - **`.name(String prefix, long start)`** — each thread the builder produces gets `prefix + counter`, the counter starting at `start` and auto-incrementing. Handy when one builder mints many threads. - **`.uncaughtExceptionHandler(Thread.UncaughtExceptionHandler)`** — what runs if the task throws an exception that escapes `run`. - **`.inheritInheritableThreadLocals(boolean)`** — whether the new thread inherits the creating thread's `InheritableThreadLocal` values (default true). Turning it off avoids accidental context/memory carry-over. - Note: virtual threads have a fixed daemon status and `NORM_PRIORITY`; the builder does not let you make them non-daemon or change priority (platform builder does). ## Producing the thread From a configured builder: - **`.start(Runnable)`** → builds and immediately **starts** the thread, returning the running `Thread`. - **`.unstarted(Runnable)`** → builds the thread in the **NEW** (not yet started) state, returning it so you can start it later with `.start()`. - **`.factory()`** → returns a **`ThreadFactory`** that applies the same configuration to every thread it creates. This is the bridge to APIs that accept a `ThreadFactory` (e.g. some executors), and it lets you reuse one configuration. ## When to prefer the builder over `Thread.startVirtualThread` `Thread.startVirtualThread(r)` is the zero-config one-liner. Reach for the builder when you need any of: 1. **Named threads** for observability/log correlation. 2. A **custom uncaught-exception handler**. 3. Control over **thread-local inheritance**. 4. A reusable **`ThreadFactory`** to hand to other APIs. 5. A **reference to the thread before it runs** (`.unstarted`), e.g. to register it somewhere first. If you need none of these, the one-liner is cleaner. ## Symmetry The same builder shape works for platform threads via `Thread.ofPlatform()`, which additionally exposes `.daemon(...)`, `.priority(...)`, `.stackSize(...)`, and `.group(...)`. Knowing both shows you understand the unified threading API, not just the virtual-thread shortcut.
- How do you give every thread from one builder a distinct, ordered name?Use .name(prefix, start) — e.g. .name("req-", 0) — and the builder appends an auto-incrementing counter, producing req-0, req-1, req-2, ... for each thread it creates.
- What does Thread.Builder.factory() return and why use it?A ThreadFactory that applies the builder's configuration to every thread it creates. Use it to hand a pre-configured (named, handler-equipped) factory to APIs that accept a ThreadFactory and to reuse one config.
saying these in an interview costs you the question
- Thinking the virtual builder can make a thread non-daemon or change priority
- Believing you must use the builder even for trivial fire-and-forget tasks
- Confusing Thread.ofVirtual() (virtual) with Thread.ofPlatform() (platform)
- Assuming .factory() starts threads — it only produces a ThreadFactory