Java并发编程(4)——死锁

本文深入探讨了死锁的概念及预防措施,包括死锁的四大必要条件、数据库中的死锁场景,以及通过加锁顺序、加锁时限与死锁检测来避免死锁的具体方法。

前言

在上一篇中,我们介绍了线程通信的一些原理。包括使用共享对象进行通信、忙等待、Object的wait()/notify()/notifyAll()方法、自旋锁、管程对象的注意事项等等。我们需要重点记忆的事情是,wait()/notify()/notifyAll()这三个方法必须在同步代码块中调用,否则会抛出IllegalMonitorStateException异常,此外,不能将字符串常量或者全局变量设置为管程对象。

1. 死锁

1.1 死锁

死锁是指两个或者是更多的线程阻塞着等待其它处于死锁状态的线程的锁,死锁通常发生在多个线程同时但并不是同一顺序请求同一组锁。
例如,线程1锁住了对象A,然后尝试给对象B加锁,但此时线程2锁住了对象B,并尝试获得对象A,这时死锁就发生了,这个例子中,两个线程永远无法成功给对象A、B加锁,并且它们也并不会知道这种情况。

我们举一个TreeNode的实例:

public class ConcurrentTreeNode {

    public ConcurrentTreeNode parent=null;

    List children=new ArrayList();

    public synchronized void addChild(ConcurrentTreeNode child){
        if (this.children.contains(child)){
            this.children.add(child);
            child.setParentOnly(this);
        }
    }

    public synchronized void addChildOnly(ConcurrentTreeNode child){
        if (this.children.contains(child)){
            this.children.add(child);
        }
    }

    public synchronized void setParent(ConcurrentTreeNode parent){
        this.parent=parent;
        parent.addChildOnly(this);
    }

    public synchronized void setParentOnly(ConcurrentTreeNode parent) {
        this.parent=parent;
    }
}

在这个例子中,如果线程1调用了parent.addChild(child)的同时有另一个线程2调用了child.setParent(parent),这里的两个线程中的parent是同一个对象,child也是同一对象,这时就会发生死锁。以下是这个过程的伪码描述:

Thread 1: parent.addChild(child); //locks parent
--> child.setParentOnly(parent);
Thread 2: child.setParent(parent); //locks child
--> parent.addChildOnly()

需要记住的是,上述过程除非是在两个线程同时调用方法时才会发生死锁,所以,程序应该在运行一段时间之后才会出现死锁,但死锁并不能预料。死锁也并非只在两个线程之间发生,多个线程之间发生相互之间等待的情况,会造成更为复杂的死锁。
我们总结一下死锁发生的四个必要条件

  • 1.互斥条件:指线程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个线程占用。如果此时还有其它线程请求资源,则请求者只能等待,直至占有资源的线程用毕释放;
  • 2.请求和保持条件:指线程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它线程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放;
  • 3.不剥夺条件:指线程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放;
  • 4.环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,即线程集合{T0,T1,T2,···,Tn}中的T0正在等待一个T1占用的资源;T1正在等待T2占用的资源,……,Tn正在等待已被T0占用的资源。

1.2数据库中的死锁

复杂的死锁场景发生在数据库事务中。一个数据库事务可能由多个SQL请求组成,在一个事务中更新一条记录,这条记录就会被锁住避免其他事务的更新请求,直到第一个事务结束。同一个事务中每一个更新请求都可能会锁住一些记录。
当多个事务同时需要对一些相同的记录做更新操作时,就很有可能发生死锁。

2.避免死锁

在有些情况下死锁是可以避免的。本文将展示三种用于避免死锁的技术:1. 加锁顺序;2. 加锁时限;3. 死锁检测

2.1顺序加锁

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。
如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个例子:

Thread 1:
lock A
lock B
Thread 2:
wait for A
lock C (when A locked)
Thread 3:
wait for A
wait for B
wait for C

如果一个线程(比如线程 3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。
例如,线程 2 和线程 3 只有在获取了锁 A 之后才能尝试获取锁 C( 译者注:获取锁 A 是获取锁 C 的必要条件 )。因为线程 1 已经拥有了锁 A,所以线程 2 和 3 需要一直等到锁 A 被释放。然后在它们尝试对 B 或 C 加锁之前,必须成功地对 A 加了锁。
按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁并对它们做适当的排序,但总有些时候是无法预知的。

2.2 加锁时限

另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行。

Thread 1 locks A
Thread 2 locks B
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked
Thread 1's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.
Thread 2's lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.

需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。

超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争,带来了新的问题。

这种机制存在一个问题,在 Java 中不能对 synchronized 同步块设置超时时间。你需要创建一个自定义锁,或使用 Java5 中 java.util.concurrent 包下的工具。

2.3 死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph 等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。
当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程 A 请求锁 7,但是锁7 这个时候被线程 B 持有,这时线程 A 就可以检查一下线程 B 是否已经请求了线程 A 当前所持有的锁。如果线程 B 确实有这样的请求,那么就是发生了死锁(线程 A 拥有锁 1,请求锁 7;线程 B 拥有锁 7,请求锁 1)。当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程 A 等待线程 B,线程 B 等待线程C,线程 C 等待线程 D,线程 D 又在等待线程 A。线程 A 为了检测死锁,它需要递进地检测所有被 B 请求的锁。从线程 B 所请求的锁开始,线程 A 找到了线程 C,然后又找到了线程 D,发现线程 D 请求的锁被线程 A 自己持有着。这是它就知道发生了死锁。
这里写图片描述

接下来的问题是,我们发现了死锁,但是我们要怎么样解决这个问题:
一种可行的方案是释放掉所有锁,回退,并且等待一段时间后重试,这个和简单的加锁时限是类似的,区别在于这里只在死锁的情况下进行回退,但如果请求同一批锁的线程太多,还是会发生死锁,因为这并不从根本上解决资源的竞争;

另一种更好的方法是,为线程设置优先级,让某些线程回退,另一些线程则保持它们的锁,如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

【顶级EI完整复现】【DRCC】考虑N-1准则的分布鲁棒机会约束低碳经济调度(Matlab代码实现)内容概要:本文介绍了名为《【顶级EI完整复现】【DRCC】考虑N-1准则的分布鲁棒机会约束低碳经济调度(Matlab代码实现)》的技术资源,聚焦于电力系统中低碳经济调度问题,结合N-1安全准则与分布鲁棒机会约束(DRCC)方法,提升调度模型在不确定性环境下的鲁棒性和可行性。该资源提供了完整的Matlab代码实现,涵盖建模、优化求解及仿真分析全过程,适用于复杂电力系统调度场景的科研复现与算法验证。文中还列举了大量相关领域的研究主题与代码资源,涉及智能优化算法、机器学习、电力系统管理、路径规划等多个方向,展示了广泛的科研应用支持能力。; 适合人群:具备一定电力系统、优化理论和Matlab编程基础的研究生、科研人员及从事能源调度、智能电网相关工作的工程师。; 使用场景及目标:①复现高水平期刊(如EI/SCI)关于低碳经济调度的研究成果;②深入理解N-1安全约束与分布鲁棒优化在电力调度中的建模方法;③开展含新能源接入的电力系统不确定性优化研究;④为科研项目、论文撰写或工程应用提供可运行的算法原型和技术支撑。; 阅读建议:建议读者结合文档提供的网盘资源,下载完整代码与案例数据,按照目录顺序逐步学习,并重点理解DRCC建模思想与Matlab/YALMIP/CPLEX等工具的集成使用方式,同时可参考文中列出的同类研究方向拓展研究思路。
内容概要:本文详细介绍了一个基于MATLAB实现的电力负荷预测项目,采用K近邻回归(KNN)算法进行建模。项目从背景意义出发,阐述了电力负荷预测在提升系统效率、优化能源配置、支撑智能电网和智慧城市建设等方面的重要作用。针对负荷预测中影响因素多样、时序性强、数据质量差等挑战,提出了包括特征工程、滑动窗口构造、数据清洗与标准化、K值与距离度量优化在内的系统性解决方案。模型架构涵盖数据采集、预处理、KNN回归原理、参数调优、性能评估及工程部署全流程,并支持多算法集成与可视化反馈。文中还提供了MATLAB环境下完整的代码实现流程,包括数据加载、归一化、样本划分、K值选择、模型训练预测、误差分析与结果可视化等关键步骤,增强了模型的可解释性与实用性。; 适合人群:具备一定MATLAB编程基础和机器学习基础知识,从事电力系统分析、能源管理、智能电网或相关领域研究的研发人员、工程师及高校师生;适合工作1-3年希望提升实际项目开发能力的技术人员; 使用场景及目标:①应用于短期电力负荷预测,辅助电网调度与发电计划制定;②作为教学案例帮助理解KNN回归在实际工程中的应用;③为新能源接入、需求响应、智慧能源系统提供数据支持;④搭建可解释性强、易于部署的轻量级预测模型原型; 阅读建议:建议结合MATLAB代码实践操作,重点关注特征构造、参数调优与结果可视化部分,深入理解KNN在时序数据中的适应性改进方法,并可进一步拓展至集成学习或多模型融合方向进行研究与优化。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值