1 基本概括
2 主要介绍
2.1 死锁的概念
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局。当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
2.2 死锁产生的原因
1) 系统资源的竞争
通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在 运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争 才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。
2) 进程推进顺序非法
进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都 会因为所需资源被占用而阻塞。
信号量使用不当也会造成死锁。进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。
3) 死锁产生的必要条件
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。
1. 互斥条件:一个资源每次只能被一个线程使用。
2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3. 不可剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
2.3 处理死锁的基本方法
(1)预防死锁:属于事前预防的策略,通过设置某些限制条件,去破坏产生死锁的四个必要条件或其中的几个条件。预防死锁比较容易实现,所以被泛使用,但是由于施加的限制条件过于严格可能会导致系统资源利用率和系统吞吐量降低。
(2)避免死锁:属于事前预防的策略,但它并不需要事先采取各种限制措施去破坏产生死锁的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的产生。但实现有一定的难度。目前较完善的系统中常用此法来避免死锁。
(3)检测死锁:这种方法不需要事前采取任何限制措施,也不用检查是否进入不安全状态,而是允许系统在运行的过程中发生死锁。但是通过系统所设置的键
测机构.及时的检测出死锁的发生,并精确地测出与死锁有关的进程和资源,然后,采取适当的措施,从系统中将已发生的死锁清除掉。
(4)解除死锁:这是与检测死锁相配套的一套措施。
当检测到系统已经产生死锁时,须将进程从死锁中解放出来。通常用到的实施方法是撤销或挂起些进程,以便收回一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。死锁的检测和解除措施,有可能使系统获得较好的资源和吞吐量,但在现实上难度也最大。
(5)预防死锁和避免死锁的区别:预防死锁和避免死锁实质上都是通过施加某种相知条件的方法,来预防发生死锁。两者的主要区别:为了预防死锁所施加的限制条件较为严格,这往往会影响到进程的并发执行,而避免死锁所施加的限制条件则较为宽松,有利于进程的并发执行。
2.3 死锁的预防
预防死锁是设法至少破坏产生死锁的四个必要条件之一严格的防止死锁的出现
1)破坏互斥条件
“互斥”条件是无法破坏的
2)破坏“占有并等待”条件
破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源
即要想出一个办法,阻止进程在持有资源的同时申请其他资源,有以下思路可提供:
方法一:
即创建进程时,要求它申请所需的全部资源,系统或满足其所有要求,或什么也不给它
方法二:
要求每个进程提出新的资源申请前,释放它所占有的资源
3)破坏“不可抢占”条件
破坏“不可抢占”条件就是允许对资源实行抢夺
如果占有某些资源的一个进程进行下一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源
如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁
4)破坏“循环等待”条件
破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。
2.4 死锁的避免
1、避免一个线程同时获取多个锁。
2、避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
3、尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
4、对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
2.5 死锁的解决方案
1)加锁顺序(线程按照一定的顺序加锁)
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。
按照顺序加锁是一种有效的死锁预防机制。
2)银行家算法(后续展开讲)
3)加锁时限
在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行。
4)死锁检测
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。
检测到死锁后的解决方案:
1)释放所有锁,回退,并且等待一段随机的时间后重试
2)给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁
检测方法
1)使用JConsole检测死锁
2) 使用JStack检测死锁
3) ThreadMXBean检测
4) 通过arthas检测
5) 通过jvisualvm检测死锁
2.6 死锁解除
1、资源剥夺法
挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但应防止被挂起的进程长时间得不到资源,而处于资源匮乏的状态。
2、撤销进程法
强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源。撤销的原则可以按进程优先级和撤销进程代价的高低进行。
3、进程回退法
让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。
3 简单用例
3.1 多线程死锁的简单案例
public class DeadLockDemo {
private static String A = "A";
private static String B = "B";
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
private void deadLock() {
Thread t1 = new Thread(() -> {
synchronized (A){
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (B){
System.out.println("1");
}
}
});
Thread t2 = new Thread(()->{
synchronized (B){
synchronized (A){
System.out.println("2");
}
}
});
t1.start();
t2.start();
}
}
3.2 设置加锁顺序
public class TransferMoneySafely {
private final Object lock = new Object();
public void transferMoney(Account fromAccount, Account toAccount, BigDecimal amount) throws InterruptedException {
if(fromAccount.getUserId() > toAccount.getUserId()) {
synchronized (fromAccount) {
Thread.sleep(2000);
synchronized (toAccount) {
doTransfer(fromAccount,toAccount, amount);
}
}
} else if(fromAccount.getUserId() < toAccount.getUserId()) {
synchronized (toAccount) {
Thread.sleep(2000);
synchronized (fromAccount) {
doTransfer(fromAccount,toAccount, amount);
}
}
} else {
synchronized (lock) {
synchronized (fromAccount){
Thread.sleep(2000);
synchronized (toAccount) {
doTransfer(fromAccount,toAccount, amount);
}
}
}
}
}
public void doTransfer(Account fromAccount, Account toAccount, BigDecimal amount) {
if(fromAccount.getAmount().compareTo(amount) < 0) {
throw new RuntimeException("转账金额错误");
} else {
fromAccount.debit(amount);
toAccount.credit(amount);
System.out.println("fromAccount amount is " + fromAccount.getAmount() + ", toAccount amount is " + toAccount.getAmount());
}
}
public static void main(String[] agrs) {
TransferMoneySafely transferMoneySafely = new TransferMoneySafely();
Account fromAccount = new Account(1231231233, new BigDecimal("10000"));
Account toAccount = new Account(1231231231, new BigDecimal("10000"));
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(
new Runnable() {
@Override
public void run() {
try {
transferMoneySafely.transferMoney(fromAccount, toAccount, new BigDecimal("2000"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
);
executorService.submit(
new Runnable() {
@Override
public void run() {
try {
transferMoneySafely.transferMoney(toAccount ,fromAccount, new BigDecimal("2000"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
);
}
}
3.3 使用lock设置加锁时间
public boolean transferMoney(Account fromAccount, Account toAccount, BigDecimal amount, long timeout, TimeUnit unit)
throws InsufficientResourcesException, InterruptedException {
//回避时间 = 固定时间+随机时间
long fixDelay = 2;
Random random = new Random();
random.setSeed(fixDelay);
long randMod = random.nextLong();
long stopTime = System.nanoTime() + unit.toNanos(timeout);
while(true) {
if(fromAccount.lock.tryLock()) {
try {
if(toAccount.lock.tryLock()) {
try {
if(fromAccount.getAmount().compareTo(amount) < 0) {
throw new InsufficientResourcesException();
} else {
fromAccount.debit(amount);
toAccount.credit(amount);
System.out.println("fromAccount amount is " + fromAccount.getAmount() + ", toAccount amount is " + toAccount.getAmount());
return true;
}
} finally {
//释放锁必须放在finally块中,否则,异常时无法释放锁
toAccount.lock.unlock();
}
}
}finally {
fromAccount.lock.unlock();
}
}
//超时退出
if(System.nanoTime() > stopTime) {
System.out.println("Time out!");
return false;
}
//回退
NANOSECONDS.sleep(fixDelay + random.nextLong() % randMod);
}
}
3.4 检测死锁
public static void main(String[] args) {
ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
// 只能检测 synachrozied 同步代码块 的死锁
// long[] deadlockedThreadIds = mbean.findMonitorDeadlockedThreads();
// 可以检测 juc下的 Lock造成的死锁和 synachrozied代码块的死锁
long[] deadlockedThreadIds = mbean.findDeadlockedThreads();
if (deadlockedThreadIds != null) {
ThreadInfo[] threadInfos = mbean.getThreadInfo(deadlockedThreadIds);
for (ThreadInfo ti : threadInfos) {
System.out.println(ti);
}
}
}
4 常见问题研究
1 什么是死锁?
2 产生死锁的原因?
3 死锁产生的4个必要条件?
4 解决死锁的基本方法
5 死锁与饥饿
常见出现的问题会在后面的文章讨论,想一起讨论学习的朋友可以点下关注,会持续更新,文章有帮助的话可以收藏,转发,有什么补充可以在下面评论,谢谢!