当前位置:首页 > 文章列表 > 文章 > java教程 > Java并发:同步方法死锁预防技巧

Java并发:同步方法死锁预防技巧

2025-10-01 09:36:28 0浏览 收藏

一分耕耘,一分收获!既然打开了这篇文章《Java并发:同步方法死锁预防技巧》,就坚持看下去吧!文中内容包含等等知识点...希望你能在阅读本文后,能真真实实学到知识或者帮你解决心中的疑惑,也欢迎大佬或者新人朋友们多留言评论,多给建议!谢谢!

Java并发:同步方法死锁预防策略

本文深入探讨了Java同步方法中因锁获取顺序不一致导致的死锁问题。通过引入一致的锁获取顺序机制,并结合java.util.concurrent.locks.Lock接口的灵活运用,提供了有效的死锁预防策略。文章详细阐述了如何基于对象唯一标识符来确定锁的获取顺序,并强调了在多线程环境中维护资源访问一致性的重要性。

理解同步方法中的死锁风险

在Java并发编程中,synchronized关键字是实现线程同步的常用机制,它确保同一时刻只有一个线程可以执行特定的代码块或方法。然而,不当的同步策略可能导致死锁,即两个或多个线程无限期地等待彼此释放资源。

一个经典的死锁场景发生在多个线程尝试获取多个锁,但获取顺序不一致时。考虑一个transferMoney方法,它需要同步两个Account对象以执行转账操作:

public class Account {
    private UUID id;
    private float balance;
    // 构造函数、getter/setter等

    public UUID getId() {
        return id;
    }

    public void debit(float amount) {
        this.balance -= amount;
    }

    public void credit(float amount) {
        this.balance += amount;
    }
}

public class TransferService {
    public void transferMoney(Account a, Account b, float value) {
        synchronized(a) { // 线程1获取了A的锁
            synchronized(b) { // 线程1尝试获取B的锁
                // 执行转账逻辑
                a.debit(value);
                b.credit(value);
            }
        }
    }
}

假设现在有两个线程同时调用transferMoney方法:

  • 线程1调用transferMoney(accountA, accountB, 100)
  • 线程2调用transferMoney(accountB, accountA, 50)

如果线程1成功获取了accountA的锁,并紧接着线程2成功获取了accountB的锁,那么:

  • 线程1会等待accountB的锁(已被线程2持有)
  • 线程2会等待accountA的锁(已被线程1持有)

这将导致典型的死锁,两个线程都无法继续执行。

策略一:强制一致的锁获取顺序

避免死锁的关键在于确保所有线程以相同的、预定义的顺序获取多个锁。这意味着我们不能依赖于方法参数的传入顺序,而应该基于锁对象的某个固有属性来确定其获取顺序。

为了实现这一点,我们可以为每个Account对象引入一个唯一标识符(例如UUID或Long ID),并约定在获取锁时,总是先获取ID较小的账户的锁,再获取ID较大的账户的锁。

首先,修改Account类,使其包含一个用于比较的唯一ID:

import java.util.Comparator;
import java.util.UUID;
import java.util.function.BinaryOperator;

public class Account {
    private UUID id;
    private float balance;

    public Account(UUID id, float initialBalance) {
        this.id = id;
        this.balance = initialBalance;
    }

    public UUID getId() {
        return id;
    }

    public void debit(float amount) {
        if (this.balance < amount) {
            throw new IllegalArgumentException("Insufficient funds.");
        }
        this.balance -= amount;
    }

    public void credit(float amount) {
        this.balance += amount;
    }

    public float getBalance() {
        return balance;
    }

    @Override
    public String toString() {
        return "Account{" + "id=" + id.toString().substring(0, 8) + ", balance=" + balance + '}';
    }

    // 辅助方法,用于确定两个账户中ID较小的那个
    public static final BinaryOperator<Account> FIRST =
        BinaryOperator.minBy(Comparator.comparing(Account::getId));

    // 辅助方法,用于确定两个账户中ID较大的那个
    public static final BinaryOperator<Account> SECOND =
        BinaryOperator.maxBy(Comparator.comparing(Account::getId));
}

接下来,修改transferMoney方法,使用FIRST和SECOND辅助方法来确定锁的获取顺序:

public class TransferService {
    public void transferMoney(Account a, Account b, float value) {
        // 确保不能向同一个账户转账
        if (a.getId().equals(b.getId())) {
            throw new IllegalArgumentException("Cannot transfer money to the same account.");
        }

        // 确定锁的获取顺序:总是先获取ID较小的账户的锁,再获取ID较大的账户的锁
        Account firstLock = Account.FIRST.apply(a, b);
        Account secondLock = Account.SECOND.apply(a, b);

        synchronized (firstLock) {
            synchronized (secondLock) {
                // 执行转账逻辑
                System.out.println(Thread.currentThread().getName() + " acquired locks for " + firstLock.getId().toString().substring(0, 8) + " and " + secondLock.getId().toString().substring(0, 8));
                try {
                    // 模拟转账耗时
                    Thread.sleep(100);
                    firstLock.debit(value);
                    secondLock.credit(value);
                    System.out.println(Thread.currentThread().getName() + " transferred " + value + " from " + a.getId().toString().substring(0, 8) + " to " + b.getId().toString().substring(0, 8));
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}

通过这种方式,无论transferMoney方法被调用时accountA和accountB的传入顺序如何,firstLock和secondLock变量总是会引用到具有一致ID顺序的账户。例如,如果accountA.id小于accountB.id,那么firstLock总是accountA,secondLock总是accountB。这样,所有线程都会以synchronized(account_with_smaller_id) { synchronized(account_with_larger_id) {...} }的顺序获取锁,从而有效避免死锁。

策略二:使用java.util.concurrent.locks.Lock

除了synchronized关键字,Java并发API还提供了java.util.concurrent.locks.Lock接口,它提供了更灵活的锁机制。Lock接口的实现(如ReentrantLock)允许更精细地控制锁的获取和释放,尤其是在处理死锁时提供了额外的工具。

Lock接口的核心思想是,当一个线程需要获取多个锁时,如果它无法一次性获取所有必需的锁,就应该释放已经持有的锁,并稍后重试。这可以通过tryLock()方法实现,它尝试获取锁而不阻塞,并返回一个布尔值指示是否成功获取。

使用Lock的基本模式如下:

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

// 假设每个Account对象内部有一个ReentrantLock
public class AccountWithLock {
    private UUID id;
    private float balance;
    private final Lock lock = new ReentrantLock(); // 每个账户一个独立的锁

    // ... 构造函数、getter/setter等

    public Lock getLock() {
        return lock;
    }
}

public class TransferServiceWithLock {
    public void transferMoney(AccountWithLock a, AccountWithLock b, float value) {
        // 同样,先确定一致的锁获取顺序
        AccountWithLock first = AccountWithLock.FIRST.apply(a, b); // 假设AccountWithLock也有FIRST/SECOND
        AccountWithLock second = AccountWithLock.SECOND.apply(a, b);

        Lock lock1 = first.getLock();
        Lock lock2 = second.getLock();

        boolean acquired1 = false;
        boolean acquired2 = false;

        try {
            // 尝试获取第一个锁
            acquired1 = lock1.tryLock();
            if (acquired1) {
                // 尝试获取第二个锁
                acquired2 = lock2.tryLock();
                if (acquired2) {
                    // 成功获取所有锁,执行转账
                    a.debit(value);
                    b.credit(value);
                } else {
                    // 未能获取第二个锁,释放第一个锁,稍后重试
                    lock1.unlock();
                }
            }
        } finally {
            // 确保所有获取的锁都被释放
            if (acquired2) {
                lock2.unlock();
            }
            if (acquired1) { // 再次检查,因为如果acquired2失败,acquired1可能仍为true
                lock1.unlock();
            }
        }
    }
}

注意事项:

  • tryLock()方法可以带超时参数,避免无限等待。
  • finally块对于确保锁的释放至关重要,即使在转账过程中发生异常。
  • 使用Lock接口时,同样需要遵循一致的锁获取顺序原则,以简化死锁处理逻辑。

总结与最佳实践

死锁是并发编程中的一个常见陷阱,但通过遵循一些基本原则可以有效避免。

  1. 统一锁获取顺序: 这是预防多资源死锁最核心的策略。通过对需要同步的资源进行排序(例如,基于对象的唯一ID),并强制所有线程按照该顺序获取锁,可以消除循环等待的条件。
  2. 避免嵌套锁: 尽量减少在持有锁的情况下再去获取另一个锁的情况。如果必须嵌套,务必确保锁的获取顺序是严格一致的。
  3. 使用java.util.concurrent.locks.Lock: 对于更复杂的并发场景,ReentrantLock等Lock实现提供了比synchronized更强大的功能,如可中断的锁获取(lockInterruptibly())、非阻塞的锁获取(tryLock())以及公平性选项。这些特性可以帮助开发者构建更健壮的死锁恢复机制。
  4. 设置锁超时: 在使用Lock时,tryLock(long timeout, TimeUnit unit)方法允许线程在指定时间内尝试获取锁。如果超时仍未获取,线程可以选择放弃并回退,而不是无限期等待。

通过深入理解死锁的成因并采纳上述策略,开发者可以显著提高并发应用程序的稳定性和可靠性。

今天带大家了解了的相关知识,希望对你有所帮助;关于文章的技术知识我们会一点点深入介绍,欢迎大家关注golang学习网公众号,一起学习编程~

SeleniumChrome代理设置全攻略SeleniumChrome代理设置全攻略
上一篇
SeleniumChrome代理设置全攻略
PHP中const与define的区别解析
下一篇
PHP中const与define的区别解析
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ChatExcel酷表:告别Excel难题,北大团队AI助手助您轻松处理数据
    ChatExcel酷表
    ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
    3193次使用
  • Any绘本:开源免费AI绘本创作工具深度解析
    Any绘本
    探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
    3405次使用
  • 可赞AI:AI驱动办公可视化智能工具,一键高效生成文档图表脑图
    可赞AI
    可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
    3436次使用
  • 星月写作:AI网文创作神器,助力爆款小说速成
    星月写作
    星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
    4543次使用
  • MagicLight.ai:叙事驱动AI动画视频创作平台 | 高效生成专业级故事动画
    MagicLight
    MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
    3814次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码