|
| 1 | +--- |
| 2 | +id: new_features.virtual_threads |
| 3 | +title: Virtual Threads |
| 4 | +slug: learn/new-features/virtual-threads |
| 5 | +type: tutorial |
| 6 | +category: awareness |
| 7 | +category_order: 1 |
| 8 | +layout: learn/tutorial.html |
| 9 | +main_css_id: learn |
| 10 | +subheader_select: tutorials |
| 11 | +toc: |
| 12 | + - Why Virtual Threads? {why} |
| 13 | + - Creating Virtual Threads {creating} |
| 14 | + - Thread API Changes {api-changes} |
| 15 | + - Capturing Task Results {task-results} |
| 16 | + - Rate Limiting {rate-limiting} |
| 17 | + - Pinning {pinning} |
| 18 | + - Thread Locals {thread-locals} |
| 19 | + - Conclusion {conclusion} |
| 20 | +description: "Virtual Threads: What, Why, and How?" |
| 21 | +author: ["CayHorstmann"] |
| 22 | +--- |
| 23 | + |
| 24 | +<a id="why"> </a> |
| 25 | +## Why Virtual Threads? |
| 26 | + |
| 27 | +When Java 1.0 was released in 1995, its API had about a hundred classes, among them `java.lang.Thread`. Java was the first mainstream programming language that directly supported concurrent programming. |
| 28 | + |
| 29 | +Since Java 1.2, each Java thread runs on a *platform thread* supplied by the underlying operating system. (Up to Java 1.1, on some platforms, all Java threads were executed by a single platform thread.) |
| 30 | + |
| 31 | +Platform threads have nontrivial costs. They require a few thousand CPU instructions to start, and they consume a few megabytes of memory. Server applications can serve so many concurrent requests that it becomes infeasible to have each of them execute on a separate platform thread. In a typical server application, these requests spend much of their time *blocking*, waiting for a result from a database or another service. |
| 32 | + |
| 33 | +The classic remedy for increasing throughput is a non-blocking API. Instead of waiting for a result, the programmer indicates which method should be called when the result has become available, and perhaps another method that is called in case of failure. This gets unpleasant quickly, as the callbacks nest ever more deeply. |
| 34 | + |
| 35 | +JEP 425 introduced *virtual threads* in Java 19. Many virtual threads run on a platform thread. Whenever a virtual thread blocks, it is *unmounted*, and the platform thread runs another virtual thread. (The name “virtual thread” is supposed to be reminiscent of virtual memory that is mapped to actual RAM.) Virtual threads became a preview feature in Java 20 (JEP 436) and are final in Java 21. |
| 36 | + |
| 37 | +With virtual threads, blocking is cheap. When a result is not immediately available, you simply block in a virtual thread. You use familiar programming structures—branches, loops, try blocks—instead of a pipeline of callbacks. |
| 38 | + |
| 39 | +Virtual threads are useful when the number of concurrent tasks is large, and the tasks mostly block on network I/O. They offer no benefit for CPU-intensive tasks. For such tasks, consider [parallel streams](https://dev.java/learn/api/streams/parallel-streams/) or [recursive fork-join tasks](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/RecursiveTask.html). |
| 40 | + |
| 41 | +<a id="creating"> </a> |
| 42 | +## Creating Virtual Threads |
| 43 | + |
| 44 | +The factory method `Executors.newVirtualThreadPerTaskExecutor()` yields an `ExecutorService` that runs each task in a separate virtual thread. For example: |
| 45 | + |
| 46 | +```java |
| 47 | +import java.util.concurrent.*; |
| 48 | + |
| 49 | +public class VirtualThreadDemo { |
| 50 | + public static void main(String[] args) { |
| 51 | + final int NTASKS = 100; |
| 52 | + ExecutorService service = Executors.newVirtualThreadPerTaskExecutor(); |
| 53 | + for (int i = 0; i < NTASKS; i++) { |
| 54 | + service.submit(() -> { |
| 55 | + long id = Thread.currentThread().threadId(); |
| 56 | + LockSupport.parkNanos(1_000_000_000); |
| 57 | + System.out.println(id); |
| 58 | + }); |
| 59 | + } |
| 60 | + service.close(); |
| 61 | + } |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +By the way, the code uses `LockSupport.parkNanos` instead of `Thread.sleep` so that we don't have to catch the pesky `InterruptedException`. |
| 66 | + |
| 67 | +Perhaps you are using a lower-level API that asks for a thread factory. To obtain a factory for virtual threads, use the new `Thread.Builder` class: |
| 68 | + |
| 69 | +```java |
| 70 | +Thread.Builder builder = Thread.ofVirtual().name("request-", 1); |
| 71 | +ThreadFactory factory = builder.factory(); |
| 72 | +``` |
| 73 | + |
| 74 | +Now, calling `factory.newThread(myRunnable)` creates a new (unstarted) virtual thread. The `name` method configures the builder to set thread names `request-1`, `request-2`, and so on. |
| 75 | + |
| 76 | +You can also use a builder to create a single virtual thread: |
| 77 | + |
| 78 | +```java |
| 79 | +Thread t = builder.unstarted(myRunnable); |
| 80 | +``` |
| 81 | + |
| 82 | +Alternatively, if you want to start the thread right away: |
| 83 | + |
| 84 | +```java |
| 85 | +Thread t = builder.started(myRunnable); |
| 86 | +``` |
| 87 | + |
| 88 | +Finally, for a quick demo, there is a convenience method: |
| 89 | + |
| 90 | +```java |
| 91 | +Thread t = Thread.startVirtualThread(myRunnable); |
| 92 | +``` |
| 93 | + |
| 94 | +Note that only the first approach, with an executor service, works with result-bearing tasks (callables). |
| 95 | + |
| 96 | +<a id="api-changes"> </a> |
| 97 | +## Thread API Changes |
| 98 | + |
| 99 | +After a series of experiments with different APIs, the designers of Java virtual threads decided to simply reuse the familiar `Thread` API. A virtual thread is an instance of `Thread`. Cancellation works the same way as for platform threads, by calling `interrupt`. As always, the thread code must check the “interrupted” flag or call a method that does. (Most blocking methods do.) |
| 100 | + |
| 101 | +There are a few differences. In particular, all virtual threads: |
| 102 | + |
| 103 | +* Are in a single thread group |
| 104 | +* Have priority `NORM_PRIORITY` |
| 105 | +* Are daemon threads |
| 106 | + |
| 107 | +There is no API for constructing a virtual thread with another thread group. Trying to call `setPriority` or `setDaemon` on a virtual thread has no effect. |
| 108 | + |
| 109 | +The static `Thread::getAllStackTraces` method returns a map of stack traces of all *platform* threads. Virtual threads are not included. |
| 110 | + |
| 111 | +A new `Thread::isVirtual` instance method tells whether a thread is virtual. |
| 112 | + |
| 113 | +Note that there is no way to find the platform thread on which a virtual thread executes. |
| 114 | + |
| 115 | +Java 19 has a couple of changes to the `Thread` API that have nothing to do with virtual threads: |
| 116 | + |
| 117 | +* There are now instance methods `join(Duration)` and `sleep(Duration)`. |
| 118 | +* The non-final `getId` method is deprecated since someone might override it to return something other than the thread ID. Call the final `threadId` method instead. |
| 119 | + |
| 120 | +As of Java 20, the `stop`, `suspend`, and `resume` methods throw an `UnsupportedOperationException` for both platform and virtual threads. These methods have been deprecated since Java 1.2 and deprecated for removal since Java 18. |
| 121 | + |
| 122 | +<a id="task-results"> </a> |
| 123 | +## Capturing Task Results |
| 124 | + |
| 125 | +You often want to combine the results of multiple concurrent tasks: |
| 126 | + |
| 127 | +```java |
| 128 | +Future<T1> f1 = service.submit(callable1); |
| 129 | +Future<T2> f2 = service.submit(callable2); |
| 130 | +result = combine(f1.get(), f2.get()); |
| 131 | +``` |
| 132 | + |
| 133 | +Before virtual threads, you might have felt bad about the blocking `get` calls. But now blocking is cheap. Here is a sample program with a more concrete example: |
| 134 | + |
| 135 | +```java |
| 136 | +import java.util.concurrent.*; |
| 137 | +import java.net.*; |
| 138 | +import java.net.http.*; |
| 139 | + |
| 140 | +public class VirtualThreadDemo { |
| 141 | + public static void main(String[] args) throws InterruptedException, ExecutionException { |
| 142 | + ExecutorService service = Executors.newVirtualThreadPerTaskExecutor(); |
| 143 | + Future<String> f1 = service.submit(() -> get("https://horstmann.com/random/adjective")); |
| 144 | + Future<String> f2 = service.submit(() -> get("https://horstmann.com/random/noun")); |
| 145 | + String result = f1.get() + " " + f2.get(); |
| 146 | + System.out.println(result); |
| 147 | + service.close(); |
| 148 | + } |
| 149 | + |
| 150 | + private static HttpClient client = HttpClient.newHttpClient(); |
| 151 | + |
| 152 | + public static String get(String url) { |
| 153 | + try { |
| 154 | + var request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); |
| 155 | + return client.send(request, HttpResponse.BodyHandlers.ofString()).body(); |
| 156 | + } catch (Exception ex) { |
| 157 | + var rex = new RuntimeException(); |
| 158 | + rex.initCause(ex); |
| 159 | + throw rex; |
| 160 | + } |
| 161 | + } |
| 162 | +} |
| 163 | +``` |
| 164 | + |
| 165 | +If you have a list of tasks with the same result type, you can use the `invokeAll` method and then call `get` on each `Future`: |
| 166 | + |
| 167 | +```java |
| 168 | +List<Callable<T>> callables = ...; |
| 169 | +List<T> results = new ArrayList<>(); |
| 170 | +for (Future<T> f : service.invokeAll(callables)) |
| 171 | + results.add(f.get()); |
| 172 | +``` |
| 173 | + |
| 174 | +Again, a more concrete sample program: |
| 175 | + |
| 176 | +```java |
| 177 | +import java.util.*; |
| 178 | +import java.util.concurrent.*; |
| 179 | +import java.net.*; |
| 180 | +import java.net.http.*; |
| 181 | + |
| 182 | +public class VirtualThreadDemo { |
| 183 | + public static void main(String[] args) throws InterruptedException, ExecutionException { |
| 184 | + ExecutorService service = Executors.newVirtualThreadPerTaskExecutor(); |
| 185 | + List<Callable<String>> callables = new ArrayList<>(); |
| 186 | + final int ADJECTIVES = 4; |
| 187 | + for (int i = 1; i <= ADJECTIVES; i++) |
| 188 | + callables.add(() -> get("https://horstmann.com/random/adjective")); |
| 189 | + callables.add(() -> get("https://horstmann.com/random/noun")); |
| 190 | + List<String> results = new ArrayList<>(); |
| 191 | + for (Future<String> f : service.invokeAll(callables)) |
| 192 | + results.add(f.get()); |
| 193 | + System.out.println(String.join(" ", results)); |
| 194 | + service.close(); |
| 195 | + } |
| 196 | + |
| 197 | + private static HttpClient client = HttpClient.newHttpClient(); |
| 198 | + |
| 199 | + public static String get(String url) { |
| 200 | + try { |
| 201 | + var request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); |
| 202 | + return client.send(request, HttpResponse.BodyHandlers.ofString()).body(); |
| 203 | + } catch (Exception ex) { |
| 204 | + var rex = new RuntimeException(); |
| 205 | + rex.initCause(ex); |
| 206 | + throw rex; |
| 207 | + } |
| 208 | + } |
| 209 | +} |
| 210 | +``` |
| 211 | + |
| 212 | +<a id="rate-limiting"> </a> |
| 213 | +## Rate Limiting |
| 214 | + |
| 215 | +Virtual threads improve application throughput since you can have many more concurrent tasks than with platform threads. That can put pressure on the services that the tasks invoke. For example, a web service may not tolerate huge numbers of concurrent requests. |
| 216 | + |
| 217 | +With platform threads, an easy (if crude) tuning factor is the size of the thread pool for those tasks. But you should not pool virtual threads. Scheduling tasks on virtual threads that are then scheduled on platform threads is clearly inefficient. And what is the upside? To limit the number virtual threads to the smallish number of concurrent requests that your service tolerates? Then why are you using virtual threads in the first place? |
| 218 | + |
| 219 | +With virtual threads, you should use alternative mechanisms for controlling access to limited resources. Instead of an overall limit on concurrent tasks, protect each resource in an appropriate way. For database connections, the connection pool may already do the right thing. When accessing a web service, you know your service, and can provide appropriate rate limiting. |
| 220 | + |
| 221 | +As an example, on my personal web site, I provide demo services for producing random items. If a large number of requests comes at an instant from the same IP address, the hosting company blacklists the IP address. |
| 222 | + |
| 223 | +The following sample program shows rate limiting with a simple semaphore that allows a small number of concurrent requests. When the maximum is exceeded, the `acquire` method blocks, but that is ok. With virtual threads, blocking is cheap. |
| 224 | + |
| 225 | +```java |
| 226 | +import java.util.*; |
| 227 | +import java.util.concurrent.*; |
| 228 | +import java.net.*; |
| 229 | +import java.net.http.*; |
| 230 | + |
| 231 | +public class RateLimitDemo { |
| 232 | + public static void main(String[] args) throws InterruptedException, ExecutionException { |
| 233 | + ExecutorService service = Executors.newVirtualThreadPerTaskExecutor(); |
| 234 | + List<Future<String>> futures = new ArrayList<>(); |
| 235 | + final int TASKS = 250; |
| 236 | + for (int i = 1; i <= TASKS; i++) |
| 237 | + futures.add(service.submit(() -> get("https://horstmann.com/random/word"))); |
| 238 | + for (Future<String> f : futures) |
| 239 | + System.out.print(f.get() + " "); |
| 240 | + System.out.println(); |
| 241 | + service.close(); |
| 242 | + } |
| 243 | + |
| 244 | + private static HttpClient client = HttpClient.newHttpClient(); |
| 245 | + |
| 246 | + private static final Semaphore SEMAPHORE = new Semaphore(20); |
| 247 | + |
| 248 | + public static String get(String url) { |
| 249 | + try { |
| 250 | + var request = HttpRequest.newBuilder().uri(new URI(url)).GET().build(); |
| 251 | + SEMAPHORE.acquire(); |
| 252 | + try { |
| 253 | + Thread.sleep(100); |
| 254 | + return client.send(request, HttpResponse.BodyHandlers.ofString()).body(); |
| 255 | + } finally { |
| 256 | + SEMAPHORE.release(); |
| 257 | + } |
| 258 | + } catch (Exception ex) { |
| 259 | + ex.printStackTrace(); |
| 260 | + var rex = new RuntimeException(); |
| 261 | + rex.initCause(ex); |
| 262 | + throw rex; |
| 263 | + } |
| 264 | + } |
| 265 | +} |
| 266 | +``` |
| 267 | + |
| 268 | +<a id="pinning"> </a> |
| 269 | +## Pinning |
| 270 | + |
| 271 | +The virtual thread scheduler mounts virtual threads onto carrier threads. By default, there are as many carrier threads as there are CPU cores. You can tune that count with the `jdk.virtualThreadScheduler.parallelism` VM option. |
| 272 | + |
| 273 | +When a virtual thread executes a blocking operation, it is supposed to be unmounted from its its carrier thread, which can then execute a different virtual thread. However, there are situations where this unmounting is not possible. In some situations, the virtual thread scheduler will compensate by starting another carrier thread. For example, in JDK 21, this happens for many file I/O operations, and when calling `Object.wait`. You can control the maximum number of carrier threads with the `jdk.virtualThreadScheduler.maxPoolSize` VM option. |
| 274 | + |
| 275 | +A thread is called *pinned* in either of the two following situations: |
| 276 | + |
| 277 | +1. When executing a `synchronized` method or block |
| 278 | +2. When calling a native method or foreign function |
| 279 | + |
| 280 | +Being pinned is not bad in itself. But when a pinned thread blocks, it cannot be unmounted. The carrier thread is blocked, and, in Java 21, no additional carrier thread is started. That leaves fewer carrier threads for running virtual threads. |
| 281 | + |
| 282 | +Pinning is harmless if `synchronized` is used to avoid a race condition in an in-memory operation. However, if there are blocking calls, it would be best to replace `synchronized` with a `ReentrantLock`. This is of course only an option if you have control over the source code. |
| 283 | + |
| 284 | +To find out whether pinned threads are blocked, start the JVM with one of the options |
| 285 | + |
| 286 | +```shell |
| 287 | +-Djdk.tracePinnedThreads=short |
| 288 | +-Djdk.tracePinnedThreads=full |
| 289 | +``` |
| 290 | + |
| 291 | +You get a stack trace that shows when a pinned thread blocks: |
| 292 | + |
| 293 | +```shell |
| 294 | +... |
| 295 | +org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) <== monitors:1 |
| 296 | +... |
| 297 | +``` |
| 298 | + |
| 299 | +Note that you get only one warning per pinning location! |
| 300 | + |
| 301 | +Alternatively, record with Java Flight Recorder, view with your favorite mission control viewer, and look for `VirtualThreadPinned` and `VirtualThreadSubmitFailed` events. |
| 302 | + |
| 303 | +The JVM will eventually be implemented so that `synchronized` methods or blocks no longer lead to pinning. Then you only need to worry about pinning for native code. |
| 304 | + |
| 305 | +The following sample program shows pinning in action. We launch a number of virtual threads that sleep in a synchronized method, blocking their carrier threads. A number of virtual threads are added that do no work at all. But they can't be scheduled because the carrier thread pool has been completely exhausted. Note that the problem goes away when you |
| 306 | + |
| 307 | +* use a `ReentrantLock` |
| 308 | +* don't use virtual threads |
| 309 | + |
| 310 | +```java |
| 311 | +import java.util.concurrent.*; |
| 312 | +import java.util.concurrent.locks.*; |
| 313 | + |
| 314 | +public class PinningDemo { |
| 315 | + public static void main(String[] args) throws InterruptedException, ExecutionException { |
| 316 | + ExecutorService service = |
| 317 | + Executors.newVirtualThreadPerTaskExecutor(); |
| 318 | + // Executors.newCachedThreadPool(); |
| 319 | + |
| 320 | + final int TASKS = 20; |
| 321 | + long start = System.nanoTime(); |
| 322 | + for (int i = 1; i <= TASKS; i++) { |
| 323 | + service.submit(() -> block()); |
| 324 | + // service.submit(() -> rblock()); |
| 325 | + } |
| 326 | + for (int i = 1; i <= TASKS; i++) { |
| 327 | + service.submit(() -> noblock()); |
| 328 | + } |
| 329 | + service.close(); |
| 330 | + long end = System.nanoTime(); |
| 331 | + System.out.printf("%.2f%n", (end - start) * 1E-9); |
| 332 | + } |
| 333 | + |
| 334 | + public static synchronized void block() { |
| 335 | + System.out.println("Entering block " + Thread.currentThread()); |
| 336 | + LockSupport.parkNanos(1_000_000_000); |
| 337 | + System.out.println("Exiting block " + Thread.currentThread()); |
| 338 | + } |
| 339 | + private static Lock lock = new ReentrantLock(); |
| 340 | + public static void rblock() { |
| 341 | + lock.lock(); |
| 342 | + try { |
| 343 | + System.out.println("Entering rblock " + Thread.currentThread()); |
| 344 | + LockSupport.parkNanos(1_000_000_000); |
| 345 | + System.out.println("Exiting rblock " + Thread.currentThread()); |
| 346 | + } finally { |
| 347 | + lock.unlock(); |
| 348 | + } |
| 349 | + } |
| 350 | + public static void noblock() { |
| 351 | + System.out.println("Entering noblock " + Thread.currentThread()); |
| 352 | + LockSupport.parkNanos(1_000_000_000); |
| 353 | + System.out.println("Exiting noblock " + Thread.currentThread()); |
| 354 | + } |
| 355 | +} |
| 356 | +``` |
| 357 | + |
| 358 | +<a id="thread-locals"> </a> |
| 359 | +## Thread Locals |
| 360 | + |
| 361 | +A *thread-local variable* is an object whose `get` and `set` methods access a value that depends on the current thread. Why would you want such a thing instead of using a global or local variable? The classic application is a service that is not threadsafe, such as `SimpleDateFormat`, or that would suffer from contention, such as a random number generator. Per-thread instances can perform better than a global instance that is protected by a lock. |
| 362 | + |
| 363 | +Another common use for thread locals is to provide “implicit” context, such as a database connection, that is properly configured for each task. Instead of passing the context from one method to another, the task code simply reads the thread-local variable whenever it needs to access the database. |
| 364 | + |
| 365 | +Thread locals can be a problem when migrating to virtual threads. There will likely be far more virtual threads than threads in a thread pool, and now you have many more thread-local instances. In such a situation, you should rethink your sharing strategy. |
| 366 | + |
| 367 | +To locate uses of thread locals in your app, run with the VM flag `jdk.traceVirtualThreadLocals`. You will get a stack trace when a virtual thread mutates a thread-local variable. |
| 368 | + |
| 369 | +<a id="conclusion"> </a> |
| 370 | +## Conclusion |
| 371 | + |
| 372 | +* Use virtual threads to increase throughput when you have many tasks that mostly block on network I/O |
| 373 | +* The primary benefit is the familiar “synchronous” programming style, without callbacks |
| 374 | +* Don't pool virtual threads; use other mechanisms for rate limiting |
| 375 | +* Check for pinning and mitigate if necessary |
| 376 | +* Minimize thread-local variables in virtual threads |
0 commit comments