CodeGym /课程 /JAVA 25 SELF /Livelock 与 Starvation:定义与示例

Livelock 与 Starvation:定义与示例

JAVA 25 SELF
第 53 级 , 课程 1
可用

1. 认识 Livelock

如果 deadlock 是线程彼此相互等待、永远停在那里,那么 livelock(活锁)则是线程看起来“活着”,不断做事、相互礼让,但……没有人真正向前推进!想象两位很有礼貌的人在狭窄走廊中:“哎,请您先走!”——“不,您先!”——“不,您先!”——如此循环不止。

形式化定义

Livelock:线程没有被阻塞,但由于不断根据其他线程的动作改变自身状态,导致无法完成工作。它们“活着”、在积极响应,但没有产生有效进展。

实际表现如何?

  • 线程不会永久阻塞,但会陷入无休止的相互让步循环。
  • 系统没有“卡死”,却也没有完成应做的工作。

生活中的例子

  • 两个机器人需要在狭窄通道里错身而过,但它们每次都会同时朝对方那一侧挪一步,结果再次互相干扰。
  • 两个线程每次发现资源被占用时就彼此让步……无穷无尽。

2. Java 中的 livelock 示例

我们来在代码里模拟一个 livelock。为简单起见,假设两个“工人”需要同一把勺子。不同于 deadlock,如果勺子被占用,他们会很“礼貌”地让出并重试——但会同步地一起这么做。

代码示例:“礼貌的工人”

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() {
            // 使用勺子(什么也不做)
        }
    }

    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 (spoon.getOwner() != this) {
                    try {
                        Thread.sleep(1); // 等待勺子空出来
                    } catch (InterruptedException ignored) {}
                    continue;
                }
                // 如果对方还饿——把勺子让给他
                if (other.isHungry()) {
                    System.out.println(name + ":把勺子让给 " + other.getName());
                    spoon.setOwner(other);
                    continue;
                }
                // 开吃!
                System.out.println(name + ":我在吃!");
                spoon.use();
                isHungry = false;
                System.out.println(name + ":我吃饱了!");
                spoon.setOwner(other);
            }
        }
    }

    public static void main(String[] args) {
        final Worker alice = new Worker("爱丽丝");
        final Worker bob = new Worker("鲍勃");
        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();
    }
}

发生了什么?

  • 爱丽丝和鲍勃都很饿,勺子最开始在爱丽丝手里。
  • 爱丽丝看到鲍勃也很饿,于是把勺子让给他。
  • 现在勺子在鲍勃手里,但他看到爱丽丝很饿,又把勺子让回去。
  • 勺子在两位工人之间来回“奔跑”,却没有人真正吃到——没有进展。

控制台输出会是什么样?

爱丽丝:把勺子让给 鲍勃
鲍勃:把勺子让给 爱丽丝
爱丽丝:把勺子让给 鲍勃
鲍勃:把勺子让给 爱丽丝
...

如何消除 livelock?

要消除 livelock,可以降低线程的“过度礼貌”。一种有效方法是在重试前添加随机暂停(例如用 Thread.sleep),这样线程就不会同步地作出反应。还可以采用更“坚持”的策略:一旦让出,就在下一次尝试前等待更久。不要把“绅士风度”用过头——过度让步同样会导致系统停滞。

3. Starvation(线程饥饿)

如果 livelock 是“永恒的礼貌”,那么 starvation(饥饿)就是一个或多个线程几乎拿不到资源或 CPU 时间,因为其他线程总是把它们压在后面。

形式化定义

Starvation:线程无法获得所需资源(CPU、内存、锁),因为其他线程不断地在它之前获得资源。结果“饥饿”的线程要么极少执行,要么几乎不执行。

导致 starvation 的原因

  • 不公平锁。 例如,普通的 synchronized 块并不保证等待最久的线程会先进入。
  • 线程优先级。 如果高优先级线程持续占用处理器,低优先级线程可能会“饿死”(setPriority)。
  • 其他线程中的无限循环。 如果某些线程从不让出 CPU(不调用 Thread.sleepThread.yield()),其他线程可能拿不到执行时间。

4. Java 中的 starvation 示例

示例:低优先级线程几乎得不到执行

public class StarvationDemo {
    public static void main(String[] args) {
        Runnable highPriorityTask = () -> {
            while (true) {
                // 密集工作,不主动让出 CPU
            }
        };

        Runnable lowPriorityTask = () -> {
            while (true) {
                System.out.println("我是低优先级线程!");
                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();
    }
}

会如何表现?

  • 高优先级线程一直在忙碌,不让出 CPU。
  • 低优先级线程几乎不执行(甚至完全不执行)。
  • 在现代 JVM/操作系统 上,调度器可能会平滑优先级的影响,但在某些系统上饥饿会十分明显。

另一个示例:因不公平锁导致的 starvation

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

    public static void main(String[] args) {
        // 5 个线程一直抢占 lock
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                while (true) {
                    synchronized (lock) {
                        // 长时间占用 lock
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException ignored) {}
                    }
                }
            }).start();
        }

        // 一个“饥饿”的线程
        new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    System.out.println("饥饿线程拿到了 lock!");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ignored) {}
                }
            }
        }).start();
    }
}

在此示例中,如果其他线程持续占用 lock,这个“饥饿”的线程可能会非常久都拿不到锁。

5. 如何发现并预防 livelock 与 starvation

如何发现?

  • Livelock:程序在运行,线程没有卡住,但没有进展(没有结果、无法跳出循环)。
  • Starvation:某些线程几乎不执行(日志里很少或没有相关输出)。

工具

  • 日志:标记工作开始/结束、资源的获取/释放。
  • 监控:VisualVMJava Mission Control——查看哪些线程活跃以及它们在做什么。
  • Thread dump:检查线程是否卡在等待 lock

如何避免?

针对 livelock:

  • 不要做过于“礼貌”的让步——在重试前添加小的随机延迟(Thread.sleep)。
  • 在重试顺序上引入随机性,摆脱线程的同步行为。
  • 使用非阻塞结构/算法(原子变量、CAS 思路)。

针对 starvation:

  • 使用“公平”的锁。例如,带公平性的 ReentrantLock
java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock(true); // 公平模式
  • 不要滥用线程优先级——更多情况下保持默认优先级。
  • 尽量缩短临界区时间(synchronized/Lock)。
  • 使用接近 FIFO 的任务队列。

表格:Deadlock、Livelock、Starvation——对比

问题 发生了什么 线程“活着”吗? 有进展吗? 典型症状
Deadlock 互相等待 程序“卡死”
Livelock 大家都在让步,但不前进 线程在运行,但没有结果
Starvation 有的在工作,其他几乎不工作 是(部分) 部分 一些线程“挨饿”

类比与有趣事实

  • Livelock——就像两个人同时向左侧迈步以便错身,结果又撞在一起。
  • Starvation——就像商店里只优先招呼“熟人”的收银员,其余人一直排队。

有趣的事实:livelockdeadlock 更少见,但更难发现——程序“没卡死”,却在做一些无效的事!

6. 处理 livelock 与 starvation 时的常见错误

错误 1:“礼貌让步”但没有延迟。 如果线程在不暂停的情况下频繁互相让步,就可能陷入 livelock。在重试获取资源前加入小的随机延迟(Thread.sleep)。

错误 2:只依赖 synchronized 等待,而不使用公平锁。 在线程很多时,普通的 synchronized 并不保证“最饥饿”的线程能先获得访问。如有需要,请使用带公平性的 ReentrantLock

错误 3:滥用线程优先级。 试图通过 setPriority “加速”重要线程,往往会让其他线程产生 starvation。没有明确需求不要随意调整优先级。

错误 4:缺乏监控与日志。 没有日志很难发现 Livelockstarvation:程序“在运行”,却没有结果。记录关键事件,并使用分析器/线程转储。

错误 5:临界区过长。 如果线程长时间持有 lock,其他线程就会等待(甚至“挨饿”)。尽量缩短 synchronized/Lock 块中的执行时间。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION