文章目录
- O、死锁定义
- 一、 常见的java死锁代码
- 1. synchronized等待对象释放,导致死锁
- 2. CountDownLatch计数等待,导致死锁
- 二、怎么避免死锁
- 2.1 死锁的四个必要条件
- 2.2 避免死锁
- 2.3 常见的避免死锁技术
- 三、java程序出现死锁,怎么解除?
- 3.1 怎么排查是否有死锁
- 3.2 死锁已经发生,怎么解除死锁?
- 四、MySQL死锁怎么解决
- 4.1 表锁死锁
- 4.2 行锁死锁
- 4.3 共享锁转排他锁
O、死锁定义
死锁定义:是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
一、 常见的java死锁代码
1. synchronized等待对象释放,导致死锁
以交易场景为例:商人A把钱拿的死死的,想等商人B把货物发出后再交钱;而商人B把货物拿的死死的,想等商人A把钱交了后才发货。两人处于互相等待释放资源状态,陷入死锁。
两个线程互相等待彼此释放自己所需要的对象,才释放自己锁住的对象,陷入死锁
- 线程1锁住object1,线程1等待线程2释放object2,才会释放object1
- 线程2锁住object2,线程2等待线程1释放object1,才会释放object2
/**
* 死锁案例
* - 线程1锁住object1,线程1等待线程2释放object2,才会释放object1
* - 线程2锁住object2,线程2等待线程1释放object1,才会释放object2
* */
public class DeadLockSynchTest_5 {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (object1) {
System.out.println(Thread.currentThread().getName() + ",object1状态:锁住");
try {
Thread.sleep(1000);
} catch (Exception e) {
System.out.println(e);
}
synchronized (object2) {//线程1锁住object1,线程1等待线程2释放object2,才会释放object1
System.out.println(Thread.currentThread().getName() + ",object2状态:锁住");
}
System.out.println(Thread.currentThread().getName() + ",object2状态:释放");
}
System.out.println(Thread.currentThread().getName() + ",object1状态:释放");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (object2) {
System.out.println(Thread.currentThread().getName() + ",object2状态:锁住");
try {
Thread.sleep(1000);
} catch (Exception e) {
System.out.println(e);
}
synchronized (object1) {//线程2锁住object2,线程2等待线程1释放object1,才会释放object2
System.out.println(Thread.currentThread().getName() + ",object1状态:锁住");
}
System.out.println(Thread.currentThread().getName() + ",object1状态:释放");
}
System.out.println(Thread.currentThread().getName() + ",object2状态:释放");
}
}).start();
}
}
输出结果
2. CountDownLatch计数等待,导致死锁
/**
* 死锁案例
* - 线程1持有资源countDownLatch1,线程1等待countDownLatch2置空,才置空countDownLatch1
* - 线程2持有资源countDownLatch2,线程2等待countDownLatch1置空,才置空countDownLatch2
* */
public class DeadLockCountDownLatchTest_5 {
public static void main(String[] args) {
CountDownLatch countDownLatch1 = new CountDownLatch(1);
CountDownLatch countDownLatch2 = new CountDownLatch(1);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ",等待countDownLatch2置空");
try {
countDownLatch2.await();
} catch (InterruptedException e) {
System.out.println(e);
}
System.out.println("countDownLatch2 已置空");
while(countDownLatch1.getCount() != 0){//释放资源countDownLatch1
countDownLatch1.countDown();
}
System.out.println("countDownLatch1 已释放");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ",等待countDownLatch1置空");
try {
countDownLatch1.await();
} catch (InterruptedException e) {
System.out.println(e);
}
System.out.println("countDownLatch1 已置空");
while(countDownLatch2.getCount() != 0){//释放资源countDownLatch2
countDownLatch2.countDown();
}
System.out.println("countDownLatch2 已释放");
}
}).start();
}
}
输出结果
二、怎么避免死锁
要想避免或打破死锁,首先得知道死锁的必要条件有哪些,只要打破其中一个就能避免或打破死锁
2.1 死锁的四个必要条件
-
互斥:一个资源不能被多个线程同时使用,只能同时被一个线程使用,其他线程只能等待。
-
请求并保持:线程在请求资源阻塞的时候,并不会释放其已经拥有的资源。
-
不可剥夺:对于线程已经获得的资源,只能线程自己释放,其他线程无法强制剥夺。
-
循环等待:两个或者两个以上线程所等待的资源出现循环依赖。
2.2 避免死锁
-
破坏“请求和保持”条件
- 方法:如果进程已经有了资源,就不要去竞争那些不可抢占的资源。比如,进程在申请资源时,一次性申请所有需要用到的资源,不要一次一次来申请,当申请的资源有一些没空,那就让线程等待。
- 缺点:比较浪费资源,进程可能经常处于饥饿状态。还有一种方法是,要求进程在申请资源前,要释放自己拥有的资源。
-
破坏“不可剥夺”条件
- 方法1:允许进程进行抢占,如果抢资源被拒绝,就释放自己的资源。
- 方法2:操作系统允许抢,只要你优先级大,可以抢到。
- 缺点:增加系统开销,且进程前段工作可能失效。
-
破坏“循环等待”条件
- 方法:统一编号系统中的所有资源,进程可在任何时刻提出资源申请,且所有申请必须按照资源的编号顺序(升序)提出。
- 缺点:限制进程对资源的请求,系统开销大。
2.3 常见的避免死锁技术
-
常见的避免死锁方式:
- 加锁顺序(给资源编号,线程按照该资源编号顺序给资源加锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
- 死锁检测
-
具体步骤:
- 每个进程、每个资源制定唯一编号
- 设定一张资源分配表,记录各进程与占用资源之间的关系
- 设置一张进程等待表,记录各进程与要申请资源之间的关系
-
典型案例:银行家算法,参见博客【银行家算法】
知识补充——银行家算法
- 一句话:当一个进程申请使用资源的时候,银行家算法通过先 试探 分配给该进程资源,然后通过安全性算法判断分配后的系统是否处于安全状态,若不安全则试探分配作废,让该进程继续等待。
- 背景:客户向银行申请贷款的额度是有限的,每个客户在第一次申请贷款时要声明“完成该项目所需的最大资金量”,在满足所有贷款要求时,客户应及时归还贷款,银行家在客户申请的贷款额度不超过自己拥有的最大值时,都应尽量满足客户需要。
- 例子:假设银行家有10万元资金,有三个客户A、B、C,分别需要8、3、9万元贷款完成项目,客户要求分期贷款,目前A已贷到4万 此时,B要申请2万,C要申请4万 银行家需要评估贷款请求的安全性,避免出现坏账。 B借2万,C借4万,能借吗?在这个例子中银行家是操作系统,资金是资源,客户是要申请资源的进程。
- 安全性逻辑判断:假设进程P1申请资源,银行家算法先试探地分配给它(先要看看当前资源池中的资源数量够不够),若申请的资源数量小于等于Available,就接着判断分配给P1后剩余的资源,看能不能使进程队列的某个进程执行完毕。(1)若没有进程可执行完毕,则系统处于不安全状态(即此时没有一个进程能够完成并释放资源,随时间推移,系统终将处于死锁状态)。(2)若有进程可执行完毕,则假设回收已分配给它的资源(剩余资源数量增加),把这个进程标记为可完成,并继续判断队列中的其它进程。(3)若所有进程都可执行完毕,则系统处于安全状态,并根据可完成进程的分配顺序生成安全序列(如{P0,P3,P2,P1}表示将申请后的剩余资源Work先分配给P0–>回收(Work+已分配给P0的资源A0=Work)–>分配给P3–>回收(Work+A3=Work)–>分配给P2–>······满足所有进程)。
如此就可避免系统存在潜在死锁的风险。
三、java程序出现死锁,怎么解除?
3.1 怎么排查是否有死锁
- 步骤1、命令"jps":显示所有当前Java虚拟机进程名及pid.
- 步骤2、命令"jstack 67918" 打印进程堆栈信息,检查一下DeadLockSynchTest_5,为什么这个进程不退栈
3.2 死锁已经发生,怎么解除死锁?
解除死锁的关键在于释放一部分死锁进程占有的资源,让其他进程能够顺利执行,常见的做法有:
1、资源剥夺法:强行挂起或撤销某些进程,释放这些进程占有的资源
2、进程回退法:让某些进程回退到能够避免死锁的地步,释放被占有的资源,这种方法也增加了实现的难度和复杂度,要求保留执行的历史信息并设置还原点。
四、MySQL死锁怎么解决
4.1 表锁死锁
-
产生原因:
用户A访问表A(锁住了表A),然后又访问表B;另一个用户B访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A释放表A才能继续,这就死锁就产生了。用户A–》A表(表锁)–》B表(表锁)
用户B–》B表(表锁)–》A表(表锁) -
解决方案:
这种死锁,除了调整程序逻辑,没有其它办法。对于多表操作,尽量按相同顺序进行处理,尽量避免同时锁定两个资源。如,操作A和B两张表时,总是按先A后B的顺序处理,必须同时锁定两个资源时,要保证在任何时刻都应该按照相同顺序锁定资源。
4.2 行锁死锁
-
产生原因1:
如果在事务中执行了一条没有索引条件的查询,引发全表扫描,把行级锁上升为全表记录锁定(等价于表级锁)。多个这样的事务执行后,就很容易产生死锁和阻塞,最终应用系统会越来越慢,发生阻塞或死锁。 -
解决方案1:
SQL 语句中不要使用太复杂的关联多表的查询;
使用 explain “执行计划"对 SQL 语句进行分析,对于有全表扫描和全表锁定的 SQL 语句,建立相应的索引进行优化。 -
产生原因2:
两个事务分别想拿到对方持有的锁,互相等待,于是产生死锁。 -
解决方案2:
(1)在同一个事务中,尽可能做到一次锁定所需要的所有资源;
(2)按照 id 对资源排序,然后按顺序进行处理。
4.3 共享锁转排他锁
-
产生原因:
事务A 查询一条纪录,然后更新该条纪录;此时事务B 也更新该条纪录,这时事务B 的排他锁由于事务A 有共享锁,必须等A 释放共享锁后才可以获取,只能排队等待。事务A 在执行更新操作时,此处发生死锁,因为事务B 已经有一个排他锁请求,并且正在等待事务A 释放其共享锁。 -
解决方案:
(1)对于按钮等控件,点击立刻失效,不让用户重复点击,避免引发同时对同一条记录多次操作;
(2)使用乐观锁进行控制。乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统性能。注意,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。