找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

140

积分

0

好友

18

主题
发表于 12 小时前 | 查看: 2| 回复: 0

在Java并发编程中,死锁是单体应用内一个棘手且危害严重的问题。本文将深入探讨死锁的本质、危害,并通过多个典型代码案例,提供具体的诊断与解决方案。

死锁问题的本质

哲学家进餐问题是一个经典的死锁模型。设想五位哲学家围坐,每人左右各有一根筷子,需同时拿到左右两根筷子才能进餐。若每位哲学家都先拿起左边的筷子,那么所有人都会因等待右边的筷子而陷入无限期的等待,形成僵局。

这个模型直观地揭示了死锁产生的四个必要条件:

  1. 互斥:一个资源每次只能被一个线程(或进程)持有。
  2. 请求与保持:线程在持有至少一个资源的同时,还在等待获取其他线程持有的资源。
  3. 不可剥夺:线程已获得的资源在未使用完之前,不能被强行剥夺。
  4. 循环等待:存在一个线程-资源的循环等待链。

在JVM中,一旦线程陷入死锁,不会像数据库那样有超时自动回滚机制,这可能导致:

  • 关键业务流程线程永久阻塞,影响系统功能。
  • 系统吞吐量下降,严重时可致服务瘫痪。

通常,只能通过重启应用来恢复,并需从代码层面根除死锁隐患。

死锁带来的危害

线程饥饿

死锁会导致大量线程长期占用CPU时间片,使其他线程得不到执行机会,引发线程饥饿。例如:

  • 高优先级线程陷入死锁,长期霸占CPU,导致低优先级线程“饿死”。
  • 某个持有锁的线程执行耗时极长的任务(如无限循环),导致所有依赖该锁的线程长时间等待。

尽管Java提供了setPriority方法设置线程优先级,但这仅为操作系统调度提供参考,实际作用有限,且不建议轻易调整。

响应时间恶化

对于计算密集型后台任务或频繁写入的热点数据操作,如果并发控制不当(如使用粗粒度锁),可能导致大量读操作被阻塞,用户感知的响应时间变长。对于GUI应用或高并发服务,可以考虑降低后台任务线程优先级,或采用更细粒度的锁(如分段锁)来分散并发压力。

活锁问题

活锁是另一种活跃性问题。线程并未阻塞,却因持续重试相同的失败操作而无法推进。例如,一个任务处理失败后被重新放回队列头部反复执行,导致后续任务永远得不到处理。

解决思路是引入随机性,打破这种循环重试的模式。例如,在分布式系统选举Leader时,各节点随机等待一段时间再发起投票,可以有效避免平票导致的反复选举。

void sentinelTimer(void) {
    // 前置检查:判断是否进入Tilt模式
    sentinelCheckTiltCondition();
    // 处理所有被监听的Redis实例
    sentinelHandleDictOfRedisInstances(sentinel.masters);
    // ...
    // 随机调整执行频率,避免多个节点同时行动
    server.hz = REDIS_DEFAULT_HZ + rand() % REDIS_DEFAULT_HZ;
}

典型死锁案例与解决方案

案例一:锁顺序死锁

这是最直观的死锁场景:两个线程以相反的顺序请求相同的两把锁。

private static final Object leftLock = new Object();
private static final Object rightLock = new Object();

public static void leftRight() {
    synchronized (leftLock) { // 线程A先获取leftLock
        synchronized (rightLock) { // 再尝试获取rightLock
            Console.log("线程A上锁成功");
        }
    }
}

public static void rightLeft() {
    synchronized (rightLock) { // 线程B先获取rightLock
        synchronized (leftLock) { // 再尝试获取leftLock
            Console.log("线程B上锁成功");
        }
    }
}

当线程A执行leftRight,线程B执行rightLeft时,便可能发生:

  1. 线程A获取leftLock
  2. 线程B获取rightLock
  3. 线程A尝试获取rightLock,等待线程B释放。
  4. 线程B尝试获取leftLock,等待线程A释放。
  5. 双方陷入永久等待。

解决方案:统一锁的获取顺序
强制所有线程都按照相同的全局顺序(例如,先获取leftLock,再获取rightLock)来申请锁,即可打破循环等待条件。

案例二:动态锁顺序死锁(转账场景)

以下是一个模拟银行转账的方法,它试图通过锁定转出账户和转入账户来保证原子性。

public static void transfer(Account from, Account to, int amount) {
    synchronized (from) {
        synchronized (to) {
            from.setMoney(from.getMoney() - amount);
            to.setMoney(to.getMoney() + amount);
        }
    }
}

问题在于,当两个线程同时进行反向转账时(如A向B转账,B同时向A转账),就会形成与案例一类似的锁顺序死锁。

解决方案:通过排序定义全局顺序
由于锁对象(Account实例)是动态传入的,我们需要定义一个固定的获取顺序。一个可靠的方案是使用对象的hashCode或唯一ID进行排序。

// 加时赛锁,用于hashCode冲突时
private static final Object tieLock = new Object();

public static void transfer(Account from, Account to, int amount) {
    int fromHash = System.identityHashCode(from);
    int toHash = System.identityHashCode(to);

    // 规定:先锁hash值小的对象
    if (fromHash < toHash) {
        synchronized (from) {
            synchronized (to) {
                doTransfer(from, to, amount);
            }
        }
    } else if (fromHash > toHash) {
        synchronized (to) {
            synchronized (from) {
                doTransfer(from, to, amount);
            }
        }
    } else {
        // hashCode相等时,使用额外的锁来保证顺序
        synchronized (tieLock) {
            synchronized (from) {
                synchronized (to) {
                    doTransfer(from, to, amount);
                }
            }
        }
    }
}
private static void doTransfer(Account from, Account to, int amount) {
    from.setMoney(from.getMoney() - amount);
    to.setMoney(to.getMoney() + amount);
}

案例三:协作对象间的嵌套锁死锁

死锁可能发生在多个协作对象的方法调用之间,这种情况更为隐蔽。例如,两个服务类AServiceBService,它们的方法会互相调用,并且每个方法自身都是synchronized的。

private static class AService {
    private int count;
    public synchronized void aFunc(BService bService) {
        bService.func(this); // 调用B服务的方法,试图获取bService的锁
        count++;
    }
    public synchronized void func(BService bService) { }
}

private static class BService {
    private int count;
    public synchronized void bFunc(AService aService) {
        aService.func(this); // 调用A服务的方法,试图获取aService的锁
        count++;
    }
    public synchronized void func(AService aService) { }
}

当线程1调用aService.aFunc(bService),线程2同时调用bService.bFunc(aService)时,很容易形成互相等待对方实例锁的死锁环路。

解决方案:缩小锁的范围(开放调用)
避免在持有锁的情况下调用外部方法(尤其是可能被其他线程持有锁的对象的方法)。只对真正需要保证原子性的代码块加锁。

private static class AService {
    private int count;
    public void aFunc(BService bService) {
        bService.func(this); // 无锁状态下调用外部方法
        synchronized (this) { // 仅对计数操作加锁
            count++;
        }
    }
    public synchronized void func(BService bService) { }
}

private static class BService {
    private int count;
    public void bFunc(AService aService) {
        aService.func(this);
        synchronized (this) {
            count++;
        }
    }
    public synchronized void func(AService aService) { }
}

这种方式打破了“请求与保持”的条件。但需要注意,将原子操作拆解后,可能需要重新审视整个业务的线程安全性。深入理解Java并发基础对于设计正确的锁策略至关重要。

死锁的诊断与工具

当应用出现线程卡顿、吞吐量骤降时,应怀疑死锁可能。排查步骤通常如下:

  1. 定位阻塞线程:使用jstack <pid>命令或Arthas等在线诊断工具,查看线程堆栈。
  2. 分析锁信息:在jstack输出的底部,通常会有明确的“Found one Java-level deadlock”提示,并列出相互等待的线程及它们持有的锁。
  3. 梳理调用链:根据堆栈信息,找到发生死锁的代码位置,并分析其调用关系。
  4. 复现与修复:结合上述死锁模型,推理出触发条件,并应用对应的解决方案进行修复。

了解操作系统级别的线程调度机制,也有助于从更底层理解死锁和线程状态。




上一篇:Kubernetes Service与kube-proxy工作原理详解:ClusterIP/NodePort流量路径剖析
下一篇:利用ASP.NET参数污染绕过WAF:JavaScript注入攻击与防御实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 15:12 , Processed in 0.130600 second(s), 37 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表