CodeGym /Courses /JAVA 25 SELF /Livelock and Starvation: definition and examples

Livelock and Starvation: definition and examples

JAVA 25 SELF
Level 53 , Lesson 1
Available

1. Introduction to livelock

If a deadlock is when threads stand still and wait for each other forever, then a livelock is when threads seem alive, constantly doing something, politely yielding to each other, but... no one makes any progress! Imagine two polite people in a narrow hallway: “Oh, after you!” — “No, you!” — “No, you!” — and so on to infinity.

Formal definition

Livelock is a situation where threads are not blocked, but due to constantly changing their state in response to other threads, they cannot finish their work. They are “alive,” actively reacting, but not doing useful work.

What does it look like in practice?

  • Threads are not blocked forever, but get stuck in an infinite loop of yielding.
  • The system does not hang, but it also does not do what it should.

Real-world analogy

  • Two robots must pass each other in a narrow corridor, and both simultaneously step aside toward each other — and keep getting in the way.
  • Two threads that each time discover the resource is busy and yield to each other... endlessly.

2. Livelock example in Java

Let’s simulate a livelock in code. For simplicity, we’ll take two “workers” who need one spoon. Unlike a deadlock, if the spoon is busy, they politely yield and try again — but together, synchronously.

Code example: “Polite workers”

public class LivelockDemo {
    static class Spoon {
        private Worker owner;

        public Spoon(Worker owner) {
            this.owner = owner;
        }

        public Worker getOwner() {
            return owner;
        }

        public synchronized void setOwner(Worker owner) {
            this.owner = owner;
        }

        public synchronized void use() {
            // Using the spoon (does nothing)
        }
    }

    static class Worker {
        private final String name;
        private boolean isHungry = true;

        public Worker(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public boolean isHungry() {
            return isHungry;
        }

        public void eatWith(Spoon spoon, Worker other) {
            while (isHungry) {
                // If I don't have the spoon — wait
                if (spoon.getOwner() != this) {
                    try {
                        Thread.sleep(1); // Wait until the spoon becomes available
                    } catch (InterruptedException ignored) {}
                    continue;
                }
                // If the other is hungry — give up the spoon
                if (other.isHungry()) {
                    System.out.println(name + ": Giving up the spoon to " + other.getName());
                    spoon.setOwner(other);
                    continue;
                }
                // Eating!
                System.out.println(name + ": I'm eating!");
                spoon.use();
                isHungry = false;
                System.out.println(name + ": I'm full!");
                spoon.setOwner(other);
            }
        }
    }

    public static void main(String[] args) {
        final Worker alice = new Worker("Alice");
        final Worker bob = new Worker("Bob");
        final Spoon spoon = new Spoon(alice);

        Thread t1 = new Thread(() -> alice.eatWith(spoon, bob));
        Thread t2 = new Thread(() -> bob.eatWith(spoon, alice));

        t1.start();
        t2.start();
    }
}

What happens?

  • Alice and Bob are both hungry; the spoon is with Alice first.
  • Alice sees that Bob is also hungry and yields the spoon.
  • Now Bob has the spoon, but he sees that Alice is hungry and yields it back.
  • The spoon “bounces” between the workers, but no one eats — no progress.

What does the output look like?

Alice: Giving up the spoon to Bob
Bob: Giving up the spoon to Alice
Alice: Giving up the spoon to Bob
Bob: Giving up the spoon to Alice
...

How to eliminate livelock?

You can eliminate a livelock by making the threads a bit less “polite.” Adding a random pause before retrying (for example, via Thread.sleep) helps — then threads stop reacting synchronously. A more “insistent” strategy also works: if you already yielded, wait longer before trying again. And don’t overdo gentlemanly behavior in algorithms — excessive yielding can also lead to stalls.

3. Starvation (thread starvation)

If livelock is “eternal politeness,” then starvation is when one or more threads do not get access to a resource or CPU at all because others are constantly getting ahead of them.

Formal definition

Starvation is a situation where a thread cannot access the required resource (CPU, memory, a lock) because other threads continually outpace it. As a result, the “starving” thread either runs extremely rarely or does not run at all.

Causes of starvation

  • Unfair locks. For example, a regular synchronized block does not guarantee that the thread that has been waiting the longest will enter first.
  • Thread priorities. If high-priority threads constantly occupy the CPU, low-priority ones can “starve” (setPriority).
  • Infinite loops in other threads. If someone never yields the CPU (does not call Thread.sleep or Thread.yield()), other threads may not get execution time.

4. Starvation example in Java

Example: A low-priority thread doesn't get to run

public class StarvationDemo {
    public static void main(String[] args) {
        Runnable highPriorityTask = () -> {
            while (true) {
                // Intensive work, never yields the CPU
            }
        };

        Runnable lowPriorityTask = () -> {
            while (true) {
                System.out.println("I am a low-priority thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ignored) {}
            }
        };

        Thread high1 = new Thread(highPriorityTask);
        Thread high2 = new Thread(highPriorityTask);
        Thread low = new Thread(lowPriorityTask);

        high1.setPriority(Thread.MAX_PRIORITY); // 10
        high2.setPriority(Thread.MAX_PRIORITY); // 10
        low.setPriority(Thread.MIN_PRIORITY);   // 1

        high1.start();
        high2.start();
        low.start();
    }
}

How does this manifest?

  • High-priority threads are busy all the time and do not yield the CPU.
  • The low-priority thread almost never runs (or does not run at all).
  • On modern JVMs/OSes, priorities can be smoothed by the scheduler, but on some systems starvation is noticeable.

Another example: starvation due to an unfair lock

public class StarvationLockDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // 5 threads that constantly acquire the lock
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                while (true) {
                    synchronized (lock) {
                        // Hold the lock for a long time
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException ignored) {}
                    }
                }
            }).start();
        }

        // One starving thread
        new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    System.out.println("Starving thread acquired the lock!");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ignored) {}
                }
            }
        }).start();
    }
}

In this example, the “starving” thread may take a very long time to get access to the lock if other threads constantly hold it.

5. How to detect and prevent livelock and starvation

How to detect?

  • Livelock: the program runs, threads do not hang, but there is no progress (no result, no loop exit).
  • Starvation: some threads rarely run (rare log messages or none at all).

Tools

  • Logging: mark start/end of work, resource acquisition/release.
  • Monitoring: VisualVM, Java Mission Control — see which threads are active and what they are doing.
  • Thread dump: check whether threads are stuck waiting on a lock.

How to avoid?

For livelock:

  • Don’t make overly “polite” yields — add a small random delay before retrying (Thread.sleep).
  • Introduce randomness into the retry order to avoid synchronous behavior of threads.
  • Use non‑blocking structures/algorithms (atomic variables, CAS approach).

For starvation:

  • Use “fair” locks. For example, ReentrantLock with fairness:
java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock(true); // fair mode
  • Don’t overuse thread priorities — prefer the default priority.
  • Minimize time inside critical sections (synchronized/Lock).
  • Use work queues where service is close to FIFO.

Table: Deadlock, Livelock, Starvation — comparison

Problem What happens Threads “alive”? Progress? Typical symptom
Deadlock Everyone waits for each other No No Program “hangs”
Livelock Everyone yields, but no one moves Yes No Threads run, but there is no result
Starvation Some run, others almost don’t Yes (some) Partial Some threads “starve”

Analogies and interesting facts

  • Livelock — like two people simultaneously stepping left to pass each other and colliding again.
  • Starvation — like a queue at a store where the cashier serves only “their own,” and the rest wait forever.

Interesting fact: livelock is less common than deadlock, but it’s harder to detect — the program doesn’t “hang,” it keeps doing something!

6. Typical mistakes when dealing with livelock and starvation

Mistake #1: “Polite yielding” without a delay. If threads yield to each other too often without a pause, they can end up in a livelock. Add a small random delay before retrying resource acquisition (Thread.sleep).

Mistake #2: Waiting only on synchronized without fair locks. With many threads, a regular synchronized does not guarantee that the “most starving” thread will get access. Use ReentrantLock with fairness if it is critical.

Mistake #3: Abusing thread priorities. Trying to “speed up” important threads via setPriority often leads to starvation for others. Don’t touch priorities without a real need.

Mistake #4: Lack of monitoring and logging. Livelock and starvation are hard to spot without logs: the program “runs,” but there is no result. Log key events and use profilers/thread dumps.

Mistake #5: Overly long critical sections. If a thread holds a lock for a long time, others will wait (or “starve”). Minimize time inside synchronized/Lock blocks.

1
Task
JAVA 25 SELF, level 53, lesson 1
Locked
Politeness Crossroad: Simulating "livelock" 🚶‍♂️🚶‍♀️
Politeness Crossroad: Simulating "livelock" 🚶‍♂️🚶‍♀️
1
Task
JAVA 25 SELF, level 53, lesson 1
Locked
Poor Background Report: An example of "starvation" caused by priorities 📉
Poor Background Report: An example of "starvation" caused by priorities 📉
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION