1. Introduction
In this tutorial, we’ll explore what it means when a thread is busy-waiting.
We’ll examine why this approach isn’t ideal and how it can lead to wasted CPU resources. Finally, we’ll discuss more effective alternatives for avoiding busy-waiting.
2. What Is Busy-Waiting?
Busy-waiting is a fundamental concept in multithreaded systems and operating systems in general.
It happens when a thread actively checks a condition in a loop until the condition becomes true. This keeps the thread “stuck”, continuously using resources without doing much work. We’ll examine busy-waiting in practice using the following test case:
@Test
void givenWorkerThread_whenBusyWaiting_thenAssertExecutedMultipleTimes() {
AtomicBoolean taskDone = new AtomicBoolean(false);
long counter = 0;
Thread worker = new Thread(() -> {
simulateThreadWork();
taskDone.set(true);
});
worker.start();
while (!taskDone.get()) {
counter++;
}
logger.info("Counter: {}", counter);
assertNotEquals(1, counter);
}
We created a new worker thread, assigned it some work to do, and then updated the flag. The main thread where the test is executed repeatedly checks that flag, and in this context, it’s busy-waiting while the worker thread is still active.
Finally, we can see that the counter is being continuously incremented. This counter indicates the number of times the main thread looped while waiting. Let’s observe the console and see its final value:
11:14:32.286 [main] INFO c.b.c.b.BusyWaitingUnitTest - Counter: 885019109
3. How to Avoid Busy-Waiting?
Now that we’ve seen the busy-waiting in action, we’ll consider more efficient approaches using blocking mechanisms. In contrast to the busy-waiting, blocking mechanisms allow a thread to pause execution until it’s explicitly resumed.
3.1. Traditional Approach: wait() and notify()
One of the most straightforward approaches is the use of the traditional, built-in wait() and notify() methods, which are inherited from the Object class.
Here’s a version of our earlier example, this time blocking a thread instead of spinning in a loop:
@Test
void givenWorkerThread_whenUsingWaitNotify_thenWaitEfficientlyOnce() {
AtomicBoolean taskDone = new AtomicBoolean(false);
final Object monitor = new Object();
long counter = 0;
Thread worker = new Thread(() -> {
simulateThreadWork();
synchronized (monitor) {
taskDone.set(true);
monitor.notify();
}
});
worker.start();
synchronized (monitor) {
while (!taskDone.get()) {
counter++;
try {
monitor.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
fail("Test case failed due to thread interruption!");
}
}
}
assertEquals(1, counter);
}
In this example, a thread running the test invokes wait(), entering the WAITING state in the Java thread lifecycle. This means it’s suspended and doesn’t perform any work until it gets notified by another thread.
Although the while loop may appear to be busy-waiting, it’s necessary to handle spurious wakeups. These are situations where a thread resumes waiting without being notified or interrupted. Because of this, it’s generally recommended to place wait() inside a loop that re-checks the condition.
Looking back at the example, when the worker thread completes its task, it sets the shared flag and calls notify() to wake the waiting thread. At the end of the test, we assert that the counter is exactly 1, confirming that the condition was efficiently checked only once.
Lastly, it’s worth noting that many blocking mechanisms raise InterruptedException. The method wait() will throw InterruptedException if the thread is interrupted while in the WAITING state. Our example uses Thread.currentThread().interrupt() to restore the thread’s interrupt status, ensuring that the interruption signal is preserved and can be detected later on.
3.2. Modern Alternatives
We used the wait() and notify() approach to demonstrate how busy-waiting can be avoided using basic thread coordination. While this method is valid in some cases, it’s worth noting that modern, high-level concurrency tools can simplify synchronization and are generally less prone to errors.
- CountDownLatch – It eliminates busy-waiting by allowing threads to block with await() until another thread signals completion by calling countDown().
- CompletableFuture – Avoids busy-waiting by design, as it doesn’t require polling or active waiting. It runs tasks asynchronously and notifies upon completion, through either non-blocking callbacks or optional blocking.
- Lock and Condition – Provides more flexible control than synchronized blocks. A thread can wait on a condition and be signaled when it’s ready, avoiding the need for constant polling.
Java also offers other blocking tools such as Semaphore, CyclicBarrier, and Phaser for more advanced coordination tasks, including managing limited resources, synchronizing multiple threads, or handling phased execution. While more specialized, they still help to avoid busy-waiting by relying on thread coordination.
4. Conclusion
In this article, we’ve explored the concept of busy-waiting in multithreaded systems.
We’ve seen how busy-waiting can waste valuable CPU resources and why it’s generally not a good synchronization strategy. By using proper blocking mechanisms, we can avoid busy-waiting and write more efficient, responsive multithreaded code.
As always, complete code examples are available over on GitHub.