【029】万字详解: 面试被问活锁和饥饿?王二把死锁当答案被刷,哇哥用食堂打饭讲透,代码直接抄!


请添加图片描述
📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌

📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta, 欢迎沟通交流

📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌

零、引入

“面试官问‘活锁和饥饿咋解决?’,我张口就说死锁要固定锁顺序,结果面试官直接摇头,说我连概念都分不清……” 王二攥着面试反馈单,上面 “对并发问题理解片面” 的评语格外刺眼,眼看到手的大厂 offer 飞了,蹲在工位旁唉声叹气。

隔壁哇哥端着刚泡好的枸杞水,拍了拍他的肩膀:“你呀,就只懂死锁,活锁和饥饿比死锁更隐蔽,面试问的概率贼高!死锁是‘互不相让,卡死不动’,活锁是‘互相谦让,啥也干不成’,饥饿是‘强者通吃,弱者没机会’—— 今天我用食堂打饭的例子给你讲透,再带你写代码复现 + 修复,下次面试让你把面试官说懵。”

点赞 + 关注,跟着哇哥和王二,搞懂活锁、饥饿和死锁的区别,面试多拿 8k,工作中再也不踩这些隐蔽坑!

请添加图片描述

一、先分清仨概念:用食堂打饭讲明白(人话版)

哇哥先给王二画了张 “食堂场景对照表”,仨概念瞬间就懂了:

在这里插入图片描述
“死锁是‘不动’,活锁是‘白动’,饥饿是‘动不了’—— 这仨的核心区别,记死了!” 哇哥敲着桌子强调。

二、活锁:互相谦让的 “无效努力”,代码复现 + 修复

请添加图片描述

🎲 活锁的真实场景:转账代码的 “无限重试”

王二之前写过一个转账代码:两个用户互相转账,检测到余额不足就立即重试,结果两个线程一直检测、一直重试,永远转不成账 —— 典型的活锁

✔️ 活锁代码复现(转账场景)

package cn.tcmeta.lock;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author: laoren
 * @description: // 活锁示例:两个用户互相转账,无限重试导致活锁
 * @version: 1.0.0
 */
public class LiveLockSample {

    // 用户类:包含余额和转账方法
    static class User {
        private final String name;
        private int balance;

        public User(String name, int balance) {
            this.name = name;
            this.balance = balance;
        }

        // 转账方法:余额不足就立即重试(活锁根源)
        public boolean transfer(User target, int amount) {
            // 检测余额是否足够
            if (this.balance < amount) {
                System.out.println(Thread.currentThread().getName() + ":" + this.name + "余额不足(" + this.balance + "),立即重试...");
                return false;
            }
            // 余额足够,执行转账
            this.balance -= amount;
            target.balance += amount;
            System.out.println(Thread.currentThread().getName() + ":" + this.name + "向" + target.name + "转账" + amount + "成功!");
            System.out.println(this.name + "余额:" + this.balance + "," + target.name + "余额:" + target.balance);
            return true;
        }

        public String getName() {
            return name;
        }

        public int getBalance() {
            return balance;
        }
    }

    public static void main(String[] args) {
        // 初始化两个用户:A有100元,B有100元
        User A = new User("用户A", 100);
        User B = new User("用户B", 100);

        // 线程1:A向B转150元(余额不足,重试)
        Runnable task1 = () -> {
            while (!A.transfer(B, 150)) {
                // 立即重试,没有延迟——活锁关键
            }
        };

        // 线程2:B向A转150元(余额不足,重试)
        Runnable task2 = () -> {
            while (!B.transfer(A, 150)) {
                // 立即重试,没有延迟
            }
        };

        // 启动线程池执行任务
        ExecutorService pool = Executors.newFixedThreadPool(2);
        pool.submit(task1);
        pool.submit(task2);

        // 运行10秒后停止(避免无限运行)
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        pool.shutdownNow();
        System.out.println("10秒后强制停止:任务完全没完成,活锁了!");
    }
}

运行结果:无限重试,永远转不成账

pool-1-thread-1:用户A余额不足(100),立即重试...
pool-1-thread-2:用户B余额不足(100),立即重试...
pool-1-thread-1:用户A余额不足(100),立即重试...
pool-1-thread-2:用户B余额不足(100),立即重试...
...(无限循环)...
10秒后强制停止:任务完全没完成,活锁了!

🎲 活锁的核心原因:“无延迟重试” 导致同步谦让

哇哥解释:“两个线程都检测到余额不足,然后立即重试,相当于你和我在走廊里同时往左边让,又同时往右边让 —— 永远同步,永远办不成事。”

💯 活锁的解决方法:加 “随机重试延迟”,打破同步

给重试逻辑加随机的延迟,让两个线程的重试时间错开,就不会同步谦让了。
请添加图片描述

📌 修复后的代码(加随机延迟)

package cn.tcmeta.lock;

import lombok.Getter;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author: laoren
 * @description: // 活锁修复:加随机重试延迟,打破同步
 * @version: 1.0.0
 */
public class LiveLockFixed {
    static class User {
        @Getter
        private final String name;
        private final AtomicInteger balance;
        private static final Random random = new Random();

        public User(String name, int balance) {
            this.name = name;
            this.balance = new AtomicInteger(balance);
        }

        public boolean transfer(User target, int amount) {
            // 原子性地完成整个转账操作
            while (true) {
                int currentBalance = balance.get();
                if (currentBalance < amount) {
                    System.out.println(Thread.currentThread().getName() + ":" + this.name + "余额不足(" + currentBalance + "),延迟重试...");
                    try {
                        TimeUnit.MILLISECONDS.sleep(random.nextInt(400) + 100);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        return false; // 中断则直接返回失败
                    }
                    continue; // 继续尝试
                }

                // CAS 操作确保转账过程的原子性
                if (balance.compareAndSet(currentBalance, currentBalance - amount)) {
                    target.balance.addAndGet(amount);
                    System.out.println(Thread.currentThread().getName() + ":" + this.name + "向" + target.name + "转账" + amount + "成功!");
                    System.out.println(this.name + "余额:" + balance.get() + "," + target.name + "余额:" + target.balance.get());
                    return true;
                }
                // 如果CAS失败说明其他线程已经改变了余额,继续循环重试
            }
        }

        public int getBalance() {
            return balance.get();
        }
    }

    public static void main(String[] args) {
        User A = new User("用户A", 200); // 调整初始余额
        User B = new User("用户B", 200);

        Runnable task1 = () -> {
            int retryCount = 0;
            while (!A.transfer(B, 150) && retryCount++ < 10) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("Task1 interrupted.");
                    break;
                }
            }
        };

        Runnable task2 = () -> {
            int retryCount = 0;
            while (!B.transfer(A, 100) && retryCount++ < 10) { // 改变转账金额避免死循环
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("Task2 interrupted.");
                    break;
                }
            }
        };

        ExecutorService pool = Executors.newFixedThreadPool(2);
        pool.submit(task1);
        pool.submit(task2);

        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            pool.shutdown();
            try {
                if (!pool.awaitTermination(1, TimeUnit.SECONDS)) {
                    pool.shutdownNow();
                }
            } catch (InterruptedException e) {
                pool.shutdownNow();
            }
        }

        System.out.println("任务完成,活锁被解决!");
        System.out.println("最终结果:用户A余额=" + A.getBalance() + ", 用户B余额=" + B.getBalance());
    }
}

运行结果:重试延迟错开,转账成功

pool-1-thread-2:用户B向用户A转账100成功!
pool-1-thread-1:用户A向用户B转账150成功!
用户B余额:250,用户A余额:150
用户A余额:150,用户B余额:250
任务完成,活锁被解决!
最终结果:用户A余额=150, 用户B余额=250

✅ 哇哥划重点:活锁的通用解决方法

  • 加随机延迟:最常用,打破线程的同步重试节奏;
  • 设置重试上限:避免无限重试,超过次数就降级(比如返回 “转账失败,请稍后再试”);
  • 优先级调整:给线程设置不同优先级,让一个线程先执行,另一个后执行。
    请添加图片描述

三、饥饿:强者通吃的 “资源垄断”,代码复现 + 修复

📌 饥饿的真实场景:线程池的 “非公平锁插队”

王二写的秒杀系统线程池,用了非公平锁的线程池,核心线程全被 VIP 用户的长任务占着,普通用户的短任务排了半小时还没执行 —— 典型的饥饿

请添加图片描述
饥饿代码复现(线程池场景)

package cn.tcmeta.lock;

import java.util.concurrent.*;

/**
 * @author: laoren
 * @description: // 饥饿示例:非公平锁线程池,长任务垄断资源,短任务饿肚子
 * @version: 1.0.0
 */
public class StarvationSample {
    // VIP长任务:执行10秒,垄断线程
    record VIPLongTask(int taskId) implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "执行VIP长任务" + taskId + "(耗时10秒)");
            try {
                TimeUnit.SECONDS.sleep(10); // 模拟长耗时
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() + "完成VIP长任务" + taskId);
        }
    }

    // 普通短任务:执行1秒,却抢不到资源
    record NormalShortTask(int taskId) implements Runnable {

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "执行普通短任务" + taskId + "(耗时1秒)");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() + "完成普通短任务" + taskId);
        }
    }

    public static void main(String[] args) {
        // 线程池配置:核心2线程,最大2线程(非公平锁,默认)
        ExecutorService pool = new ThreadPoolExecutor(
                2, 2,
                60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10),
                // 默认非公平锁的线程工厂,VIP任务优先提交,垄断资源
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        // 先提交2个VIP长任务,占满核心线程
        pool.submit(new VIPLongTask(1));
        pool.submit(new VIPLongTask(2));

        // 再提交5个普通短任务,排队等待(饥饿)
        for (int i = 1; i <= 5; i++) {
            pool.submit(new NormalShortTask(i));
        }

        // 运行15秒后停止
        try {
            TimeUnit.SECONDS.sleep(15);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        pool.shutdownNow();
        System.out.println("15秒后停止:普通短任务只执行了极少数,大部分饿肚子!");
    }

}

执行结果:

pool-1-thread-2执行VIP长任务2(耗时10秒)
pool-1-thread-1执行VIP长任务1(耗时10秒)
pool-1-thread-2完成VIP长任务2
pool-1-thread-1完成VIP长任务1
pool-1-thread-1执行普通短任务1(耗时1秒)
pool-1-thread-2执行普通短任务2(耗时1秒)
pool-1-thread-1完成普通短任务1
pool-1-thread-1执行普通短任务3(耗时1秒)
pool-1-thread-2完成普通短任务2
pool-1-thread-2执行普通短任务4(耗时1秒)
pool-1-thread-1完成普通短任务3
pool-1-thread-1执行普通短任务5(耗时1秒)
pool-1-thread-2完成普通短任务4
pool-1-thread-1完成普通短任务5
15秒后停止:普通短任务只执行了极少数,大部分饿肚子!

✔️ 饥饿的核心原因:“资源垄断”+“非公平竞争”

哇哥解释:“核心线程就 2 个,全被 VIP 长任务占了,普通短任务只能排队 —— 非公平锁下,新提交的 VIP 任务还可能插队,普通任务永远抢不到资源,这就是饥饿。”

‼️ 饥饿的解决方法:拆分线程池 + 公平锁

把长任务和短任务分开用不同的线程池,再给短任务线程池用公平锁,保证按顺序执行,不插队。

🎸 修复后的代码(拆分线程池 + 公平锁)

package cn.tcmeta.lock;

import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author: laoren
 * @description: // 饥饿修复:拆分线程池+公平锁,避免资源垄断
 * @version: 1.0.0
 */
public class StarvationFixed {
    // VIP长任务(不变)
    record VIPLongTask(int taskId) implements Runnable {

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "执行VIP长任务" + taskId + "(耗时10秒)");
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() + "完成VIP长任务" + taskId);
        }
    }

    // 普通短任务(不变)
    record NormalShortTask(int taskId) implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "执行普通短任务" + taskId + "(耗时1秒)");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() + "完成普通短任务" + taskId);
        }
    }

    public static void main(String[] args) {
        // 修复1:拆分线程池——VIP长任务专用线程池
        ExecutorService vipPool = new ThreadPoolExecutor(
                2, 2,
                60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        // 修复2:普通短任务专用线程池,用公平锁
        ExecutorService normalPool = new ThreadPoolExecutor(
                3, 3, // 多给几个核心线程
                60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10),
                // 自定义线程工厂,用公平锁的ReentrantLock
                r -> new Thread(r, "普通任务线程"),
                new ThreadPoolExecutor.CallerRunsPolicy()
        ) {
            // 重写newTaskFor,保证任务按顺序执行(公平)
            @Override
            protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
                return new FutureTask<>(runnable, value) {
                    // 公平锁保证任务按提交顺序执行
                    private final ReentrantLock lock = new ReentrantLock(true);

                    @Override
                    public void run() {
                        lock.lock();
                        try {
                            super.run();
                        } finally {
                            lock.unlock();
                        }
                    }
                };
            }
        };

        // 提交VIP长任务到专属线程池
        vipPool.submit(new VIPLongTask(1));
        vipPool.submit(new VIPLongTask(2));

        // 提交普通短任务到专属线程池(不会被VIP任务垄断)
        for (int i = 1; i <= 5; i++) {
            normalPool.submit(new NormalShortTask(i));
        }

        // 运行15秒后停止
        try {
            TimeUnit.SECONDS.sleep(15);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        vipPool.shutdownNow();
        normalPool.shutdownNow();
        System.out.println("15秒后停止:普通短任务全部执行完成,饥饿问题解决!");
    }
}

执行结果:

普通任务线程执行普通短任务1(耗时1秒)
普通任务线程执行普通短任务2(耗时1秒)
pool-1-thread-1执行VIP长任务1(耗时10秒)
普通任务线程执行普通短任务3(耗时1秒)
pool-1-thread-2执行VIP长任务2(耗时10秒)
普通任务线程完成普通短任务3
普通任务线程完成普通短任务2
普通任务线程完成普通短任务1
普通任务线程执行普通短任务4(耗时1秒)
普通任务线程执行普通短任务5(耗时1秒)
普通任务线程完成普通短任务5
普通任务线程完成普通短任务4
pool-1-thread-2完成VIP长任务2
pool-1-thread-1完成VIP长任务1
15秒后停止:普通短任务全部执行完成,饥饿问题解决!

💯 哇哥划重点:饥饿的通用解决方法

  • 拆分资源池:长任务 / 短任务、VIP / 普通任务分开用不同的线程池,避免资源垄断;
  • 使用公平锁:保证线程按顺序获取资源,不插队;
  • 设置资源上限:给长任务设置超时时间,避免长期占用资源;
  • 优先级调整:给饥饿的线程提高优先级(慎用,可能引发新的饥饿)。

四、死锁、活锁、饥饿 终极对比表(面试直接抄)

王二掏出小本本,把哇哥的总结记成表格,面试时直接答:
在这里插入图片描述

五、总结:避坑心法,看完就能用

请添加图片描述

✅ 哇哥把核心要点缩成 3 句顺口溜,王二贴在显示器上:

  • 死锁防顺序:多锁操作固定顺序,别互相抢;
  • 活锁加延迟:重试逻辑别同步,加随机延迟;
  • 饥饿分池子:长短短任务分开池,公平锁来兜底。

😭哇哥的血泪彩蛋

请添加图片描述
“我早年做支付系统,写了个无延迟重试的退款代码,” 哇哥捂脸,“结果线上出现活锁,退款请求无限重试,数据库被刷爆 —— 查了 6 小时才发现是活锁,加了随机延迟立马好。后来做秒杀,又因为线程池没拆分导致普通用户饥饿,被用户投诉到工信部,从那以后,我写并发代码必先查‘死锁、活锁、饥饿’三个坑!”

🗨️ 最后说句实在的

请添加图片描述
**死锁、活锁、饥饿是并发编程的 “三大隐形坑”,面试必问,工作中必踩。**记住:死锁是 “卡死”,活锁是 “白忙”,饥饿是 “饿肚子”,按场景用对应的解决方法,比瞎写代码靠谱 10 倍。

今天的代码你复制过去改改参数就能用,不管是转账系统、秒杀系统还是线程池,都能避开这些坑。如果帮你搞定了面试,点赞 + 分享给你那还在分不清仨概念的同事!

请添加图片描述
请添加图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值