JRebel logo and green threads
June 6, 2024

What Are Threads and Fibers in Java?

Developer Productivity
Java Application Development

Threads and fibers are often talked about in connection with one another, yet they have distinct use cases and performance considerations. 

Threads and fibers are largely the same in both their abstraction and implementation; the main difference is the use case. OS threads can be used in any language but require a lot of RAM and are slow to synchronize and to spawn, while fibers are specific to a certain language or runtime, but are very lightweight, and are synchronized with virtually no overhead. What's more, virtual threads became a permanent feature of the JDK with Java 21, which oversome some of the performance limitations of traditional threads to maximize hardware performance. 

Read on to learn more, including:

Back to top

What Is a Thread in Java?

A thread is a continuation scheduled to run on a CPU core at the appropriate time by a scheduler. A continuation is simply a program counter, marking our point in the sequence of instructions, and a stack, storing the value of our variables. 

The OS sees and uses the hardware runtime: CPUs, application data registers, code registers (i.e. the program counter), memory registers (e.g. the stack pointer), virtual memory and CPU modes. In particular, CPUs can switch between a limited user mode and a more powerful kernel mode when trap events happen or syscalls are executed. OS threads share most kernel resources (e.g. I/O descriptors) and inhabit the same address space: this makes them lighter than several single-thread OS processes and at the same time allows them to share data. They don’t share CPU registers though and have separate stacks, which allows them to execute distinct control flows concurrently.

Back to top

JVM Threads and Scheduling

There can be more threads than there are available processors, so the OS needs to swap them in and out of CPUs by means of scheduling. A scheduling event can be triggered either by a preemption event trap (when the thread has consumed its CPU lease) or when the code explicitly invokes a syscall trap (to perform a kernel routine, like I/O). The CPU switches to kernel mode and transfers execution to a special memory area containing the trap handler: the kernel has now the power of snapshotting the CPU registers, including the program counter and the thread stack pointer, and package them into a Thread Control Block continuation.

After that the scheduler gets invoked to select the next thread to run, possibly in a different process; the kernel then restores all the relevant CPU registers and sets the processor up to switch back again into user mode and to resume execution at the restored program counter’s address.

Back to top

What are Virtual Threads?

Virtual threads are lightweight threads that simplify writing and maintaining concurrent applications with high compute requirements.   

Traditional platform threads are very heavy. Virtual threads, on the other hand, move the responsibility of scheduling threads from the OS to the JVM (Java Virtual Machine), effectively introducing an abstraction layer between the OS and the application. Virtual threads became a permanent feature in the JDK with Java 21, overcoming some of the performance limitations of traditional threads to maximize hardware performance.  

📚 Further Reading: Dive into the details on virtual threads.  

Back to top

What's the Difference Between Threads and Fibers in Java?

Threads are implemented by the operating system, while fibers (or user mode threads) are implemented in user mode. 

Put plainly, fibers are threads, i.e., sequential processes that we can spawn and synchronize with others. However, usually when we say “thread” we mean those threads implemented by the operating systems, while fibers (also sometimes referred to as lightweight threads or user-mode threads) are implemented in user mode.

Back to top

What Is a Quasar Fiber?

Quasar is a user-friendly library of lightweight threads for concurrent programming in Java. 

Quasar abstracts the fiber and thread into a single strand, allowing fibers and threads to interoperate seamlessly. Quasar fibers are implemented just like OS threads, only in JVM bytecode rather than in the OS kernel. 

Why Use Quasar Fibers?

OS threads come with a heavy performance penalty: switching between threads involves jumping back and forth from user to kernel mode, possibly even across address space boundaries. These are expensive operations partly because they cause TLB flushes, cache misses and CPU pipelining havoc: that’s also why traps and syscalls can be orders of magnitude slower than regular procedure calls.

In addition, the kernel schedules threads (i.e. assigns their continuation to a CPU core) using a general-purpose scheduling algorithm, which might take into account all kinds of threads, from those serving a single transaction to those playing an entire video. 

Back to top

When to Use Fibers in Java

Fibers, because they are scheduled at the application layer, can use a scheduler that is more appropriate for their use-case. Most fibers are used to serve transactions, and as such are active for very short periods and block very often. Their behavior is often to be awakened by IO or another fiber, run a short processing cycle, and then transfer control to another fiber (using a queue or another synchronization mechanism). Such behavior is best served by a scheduler employing an algorithm called “work-stealing”; this kind of scheduler is indeed employed by Erlang, Go and Quasar (by default). When fibers behave this way, work-stealing ensures minimal cache misses when switching between fibers.

The kernel can be a high-concurrency bottleneck. Suppose that thread switches were as fast as normal procedure calls, and we could avoid maintaining kernel data structures for threads: then we could gain a lot in terms of both memory footprint and switch efficiency. 

What About JVM Fiber Performance?

Managing user-mode stacks for fibers brings some overhead; how much exactly depends on how often instrumented methods are called and how deep the fiber call stack is. For example Quasar integrations such as Comsat's are often based on the FiberAsync class which parks the fiber after an async API is invoked and unparks it when the completion callback is invoked. In this case the stack is very shallow and the call frequency is very low, because fibers are used only to perform I/O operations (which are orders of magnitude slower than a method call).

This benchmark analysis shows that serving HTTP requests with fibers rather than threads also has the benefit of significantly increasing a server’s capacity and makes it much more resilient. Our experience is that Quasar fibers work very well in many concrete situations: they allow you to write straightforward fiber-blocking code, yet at the same time develop highly (and finely) concurrent systems that simply couldn’t run on bulky OS threads. 

The overhead of fibers is higher but still very low even when compared to async and monadic APIs, which have the disadvantage of introducing a cumbersome, infectious programming style and don’t interoperate with imperative control flow constructs built into a language.

So Aren’t JVM Fibers Generators or async/awaits?

No, as we have seen fibers are real threads: namely a continuation plus a scheduler. Generators and async/awaits are implemented with continuations (often a more limited form of continuation called stackless, which can only capture a single stack frame), but those continuations don’t have a scheduler, and are therefore not threads.

Back to top

Final Thoughts

Don't let the performance improvements end with the discussion on fibers and threads. JRebel skips redeploys and rebuilds while maintaining application state. This allows developers to instantly check the results of their code changes, allowing them to stay in rhythm while developing.

See how much time you could save during your 14-day free trial. 

Try JRebel Free

Back to top