Core Java

Avoiding Busy Waiting in Java

Busy-waiting is a common but inefficient approach in concurrent programming. In this article, we explore what busy-waiting means, why it can lead to wasted CPU resources, and more effective alternatives for handling thread synchronization in Java.

1. Understanding Busy-Waiting in Java

Busy-waiting occurs when a thread continuously checks for a condition to be true without relinquishing CPU control. Instead of sleeping or blocking, the thread runs in a tight loop, consuming CPU cycles even when no useful work is being done.

For example, a program might check if a shared flag is true before continuing, repeatedly looping until it changes. While this approach can work, it consumes unnecessary CPU resources and may starve other threads.

public class BusyWaitExample {

    private static volatile boolean dataReady = false;

    public static void main(String[] args) throws InterruptedException {
        Thread producer = new Thread(() -> {
            try {
                Thread.sleep(2000); // Simulate work
                dataReady = true;
                System.out.println("Producer: Data is ready.");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread consumer = new Thread(() -> {
            long counter = 0;
            while (!dataReady) {
                counter++;
            }
            System.out.println("Consumer: Data received after looping " + counter + " times.");
        });

        producer.start();
        consumer.start();

        producer.join();
        consumer.join();
    }
}

In this example, the consumer thread continuously checks the dataReady flag in a loop, keeping the CPU busy without doing meaningful work until the flag becomes true. This is the essence of busy-waiting, which can severely impact performance in high-load environments.

The counter variable reveals how many times the loop executes before dataReady changes.

Sample Output:

Producer: Data is ready.
Consumer: Data received after looping 445514055 times.

The counter value is huge because the CPU spins in the loop millions of times during the 2-second wait, doing no useful work, which is a clear waste of resources.

1.1 Why Busy-Waiting is Problematic

Busy-waiting is generally discouraged because it wastes CPU cycles that could be allocated to other threads or processes. When a thread loops without blocking or sleeping, it prevents the CPU from efficiently scheduling tasks. This can lead to:

  • High CPU usage for no productive work.
  • Starvation of other threads needing CPU time.
  • Increased power consumption in battery-powered systems.
  • Reduced scalability in multi-threaded applications.

In modern Java applications, blocking or signalling mechanisms are preferred to let threads wait efficiently until a condition changes.

2. Avoiding Busy-Waiting with wait() and notify()

One traditional approach to avoid busy-waiting is to use the built-in wait() and notify() methods in Java. These methods allow threads to pause execution and resume only when notified, freeing the CPU for other tasks.

public class WaitNotifyExample {
    private static boolean dataReady = false;

    public static void main(String[] args) throws InterruptedException {
        final Object lock = new Object();

        Thread producer = new Thread(() -> {
            try {
                Thread.sleep(2000);
                synchronized (lock) {
                    dataReady = true;
                    System.out.println("Producer: Data is ready.");
                    lock.notify();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread consumer = new Thread(() -> {
            long counter = 0;
            synchronized (lock) {
                while (!dataReady) {
                    counter++;
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.println("Consumer: Data received after looping " + counter + " times.");
            }
        });

        producer.start();
        consumer.start();

        producer.join();
        consumer.join();
    }
}

Here, the consumer thread waits using lock.wait() instead of looping endlessly. The producer thread calls lock.notify() once the data is ready, waking the consumer without wasting CPU resources.

Sample Output:

Producer: Data is ready.
Consumer: Data received after looping 1 times.

The counter is low because the consumer waits without spinning, only looping once before being notified, resulting in minimal CPU usage.

3. Using Lock and Condition

Java’s java.util.concurrent.locks package provides Lock and Condition interfaces for more advanced control over thread synchronisation.

public class LockConditionExample {

    private static boolean dataReady = false;
    private static final Lock lock = new ReentrantLock();
    private static final Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        Thread producer = new Thread(() -> {
            try {
                Thread.sleep(2000);
                lock.lock(); // Acquire the lock before modifying shared state
                try {
                    dataReady = true;
                    System.out.println("Producer: Data is ready.");
                    condition.signal(); // Notify the waiting thread
                } finally {
                    lock.unlock(); // Ensure lock is released
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread consumer = new Thread(() -> {
            long counter = 0;
            lock.lock(); // Acquire the lock before checking or waiting on condition
            try {
                while (!dataReady) {
                    counter++;
                    condition.await(); // Releases the lock and waits
                }
                System.out.println("Consumer: Data received after looping " + counter + " times.");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock(); // Always release lock
            }
        });

        producer.start();
        consumer.start();

        producer.join();
        consumer.join();
    }
}

Lock and Condition allow fine-grained locking control. The consumer waits on condition.await() until the producer signals with condition.signal(). The lock.lock() method is used to make sure that only one thread (either the producer or the consumer) can access or change the shared variable dataReady at a time. This helps prevent problems where both threads try to read or change the value at the same time.

In the producer, lock.lock() is called before it sets dataReady = true and notifies the consumer. The lock is always released using lock.unlock() in a finally block, so even if something goes wrong, the program won’t freeze.

In the consumer, lock.lock() is used before checking if dataReady is true. If it’s not, the consumer waits using condition.await(), which automatically releases the lock while waiting. This lets the producer take the lock, update the value, and signal the consumer. When signalled, the consumer gets the lock back and checks again. The structured use of locking in both threads maintains thread safety and coordination without deadlocks.

Sample Output:

Producer: Data is ready.
Consumer: Data received after looping 1 times.

Like wait()/notify(), Condition.await() avoids constant looping, so the counter stays low.

4. Using CountDownLatch

A CountDownLatch allows one or more threads to wait until a set of operations has completed. This is useful when we want to avoid busy-waiting for a signal.

public class CountDownLatchExample {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);

        Thread producer = Thread.ofPlatform().start(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("Producer: Data is ready.");
                latch.countDown();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread consumer = Thread.ofVirtual().start(() -> {
            long counter = 0;
            try {
                counter++;
                latch.await();
                System.out.println("Consumer: Data received after looping " + counter + " times.");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.join();
        consumer.join();
    }
}

In this example, the consumer runs as a virtual thread created using Thread.ofVirtual(), increments the counter once, and then blocks on latch.await() until the producer calls latch.countDown(). During this wait, the JVM parks the virtual thread, consuming almost no CPU resources, and unparks it when the latch reaches zero, avoiding busy-waiting entirely while ensuring proper sequencing of tasks.

Sample Output:

Producer: Data is ready.
Consumer: Data received after looping 1 times.

CountDownLatch.await() blocks efficiently, resulting in minimal counter increments.

5. Using CompletableFuture

CompletableFuture is part of Java’s java.util.concurrent package and provides an efficient way to wait for asynchronous computations without busy-waiting. Unlike busy-waiting, calling get() or join() on a CompletableFuture will block efficiently until the computation is complete, freeing CPU resources for other work.

public class CompleteableFutureExample {

    public static void main(String[] args) throws InterruptedException {
        CompletableFuture<String> future = new CompletableFuture<>();

        Thread producer = new Thread(() -> {
            try {
                Thread.sleep(2000);
                future.complete("Data");
                System.out.println("Producer: Data is ready.");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread consumer = new Thread(() -> {
            long counter = 0;
            try {
                counter++; // Only increments once before blocking
                String data = future.get(); // Wait efficiently
                System.out.println("Consumer: Received " + data + " after looping " + counter + " times.");
            } catch (InterruptedException | ExecutionException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();

        producer.join();
        consumer.join();
    }
}

Here, the consumer increments the counter once and then calls future.get(). This method blocks the thread without consuming CPU cycles until the producer calls future.complete().

Output:

Producer: Data is ready.
Consumer: Received Data after looping 1 times.

The counter value is 1 because the consumer only loops once before blocking. This shows that CompletableFuture avoids busy-waiting entirely while providing a modern, asynchronous-friendly API.

6. Conclusion

In this article, we explored what busy-waiting is, why it is inefficient, and how it can waste CPU resources. We presented several Java busy waiting alternatives, such as wait()/notify(), Lock/Condition, CountDownLatch, and CompletableFuture, all of which demonstrated minimal counter values compared to the high counts in a busy-waiting loop. By using these efficient waiting mechanisms, we can achieve better CPU utilization and more effective concurrency handling in Java applications.

7. Download the Source Code

This article explores various Java busy-waiting alternatives.

Download
You can download the full source code of this example here: java busy waiting alternatives

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button