前言:
作为开发人员对死锁肯定不陌生,即使在项目中没有遇到过,但是至少也听过。死锁的出现存在着偶然性,但并不意味着程序没有存在死锁的风险(如果使用并发编程)一旦项目中出现死锁是一件非常严重的事情,它直接回导致项目卡死直至崩溃重启。今天给大家重点分享是,死锁是如何产生、如何检测死锁、以及如何避免死锁,最后会通过实例避免死锁。
- 死锁的定义
- 死锁产生的原因
- 检测死锁
- 避免死锁
一、死锁定义
举个简单例子解释死锁:现在有一双筷子,只有同时拿到一双筷子的人才能吃饭,这个时候A,B两个人都在抢这双筷子,但是不巧的是A,B都只抢到了一只筷子,这个时候必须要其中一个人把筷子放下,让另一个人拿过去,才能凑成一双筷子吃饭,但是这个时候A,B都不愿意放下自己手上的筷子都在等对方放下,最终都没吃上饭饿死了,这个时候就形成了死锁。
通过上面的例子我们来分析一下死锁产生的必要条件
1,资源一定是要>1,比如这个时候A、B两个人抢一个勺子喝汤,这个时候就不存在死锁问题
2,线程数>1,当线程数<=1时其实就是单线程了,肯定同样不存在死锁的问题
二、死锁产生的原因
1、死锁产生的根本原因是获取锁的顺序不一致
同样拿上面一个例子来说,现在更改一下规则,一双筷子分为a1、a2两只,抢筷子必须要先抢到a1,才能去抢a2,同样是A,B两个人抢一双筷子,这个时候就不会产生死锁的问题,因为定义了枪锁的顺序,比如A抢到了a1,这个时候B只能等待a1被释放了才能去抢a1。
2、死锁的实例
(1)静态死锁
MyDeadLock .java
package com.concurrent.deadlock;
public class MyDeadLock {
//lock1
private final Object firstLock = new Object();
//lock2
private final Object secondLock = new Object();
//先获取lock1,再获取lock2
private void first2SecondLock() throws InterruptedException {
synchronized (firstLock) {
Thread.sleep(100);//保证获取锁时间充分
System.out.println(Thread.currentThread().getName()+" get firstLock "
+ "ready get secondLock...");
synchronized (secondLock) {
System.out.println(Thread.currentThread().getName()+" get secondLock end ");
}
}
}
//先获取lock2,再获取lock1
private void second2First() throws InterruptedException {
synchronized (secondLock) {
Thread.sleep(100);//保证获取锁时间充分
System.out.println(Thread.currentThread().getName()+" get secondLock "
+ "ready get first lock...");
synchronized (firstLock) {
System.out.println(Thread.currentThread().getName()+" get firstLock end ");
}
}
}
public static void main(String[] args) {
MyDeadLock myDeadLock = new MyDeadLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
myDeadLock.first2SecondLock();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
myDeadLock.second2First();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
运行结果:
上面就是最简单的死锁,thread0获取了firstLock,thread1获取了secondLock,都在等对方释放。因为顺序是写死的,我这里暂时称作它为静态死锁,对应静态当然有动态死锁拉。
(2)动态死锁
静态死锁一般不会出现在我们的程序中,因为太简单了,谁也不会犯这种低级错误,出现比较多的可能是动态死锁,并且出现的概率不高,不易复现
动态死锁当然也是因为获取锁的顺序不一致导致,只是它给人造成的假象,让人认为是顺序的获取了锁。这里举个微信转账的例子,我们人为定义“获取锁顺序一致”(注意啊,这里是引号)就是每次转账先锁定转出帐户,再锁定转入帐户,理论上是做到了获取锁顺序一致性,但是当出现A向B转账的同时B也在向A转账(或者是环形死锁),就出现了死锁情况,虽然平时这种概率出现很小,但是也是存在风险,例如微信过年发红包这种概率不算小把,下面看下动态死锁部分的代码。
DynamicDeadLockTransfer .java
package com.concurrent.dynamicdeadlock.service;
import com.concurrent.dynamicdeadlock.UserAccount;
/**
* @author hongtaolong
* 动态死锁转账
*/
public class DynamicDeadLockTransfer implements ITransfer {
@Override
public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
synchronized (from) {//锁定转出帐户
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+" get lock:"+from.getName()+"...");
synchronized (to) {//锁定转入帐户
from.flyMoney(amount);
to.addMoney(amount);
System.out.println(Thread.currentThread().getName()+" get lock:"+to.getName()+" end");
System.out.println("transfer success amount = "+amount);
}
}
}
}
测试代码:
DnynamicDeadLockTest .java
package com.concurrent.dynamicdeadlock;
import com.concurrent.dynamicdeadlock.service.DynamicDeadLockTransfer;
import com.concurrent.dynamicdeadlock.service.ITransfer;
public class DnynamicDeadLockTest {
public static void main(String[] args) {
UserAccount zhangsan = new UserAccount("zhangsan", 10000);
UserAccount lisi = new UserAccount("lisi", 10000);
ITransfer transfer = new DynamicDeadLockTransfer();
TransferThread t1 = new TransferThread(zhangsan, lisi, 100, transfer);
TransferThread t2 = new TransferThread(lisi, zhangsan, 150, transfer);
t1.start();
t2.start();
}
static class TransferThread extends Thread{
private final UserAccount from;
private final UserAccount to;
private final double amount;
private final ITransfer transfer;
public TransferThread(UserAccount from,UserAccount to,double amount,ITransfer transfer) {
this.from = from;
this.to = to;
this.amount = amount;
this.transfer = transfer;
}
@Override
public void run() {
try {
transfer.transfer(from, to, amount);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
运行结果:
其实还是获取锁的顺序不一致导致的死锁
三、检测死锁
检测死锁的方法jdk给我们提供了好几种,有可视化工具jconsule、jvisualvm,大家可自行查阅如何使用,我今天简单介绍下通过命令行
cmd进入到jdk的bin目录
1、jps指令获取进程id
2、jstack id指令查看指定的进程
这个截图已经描述的很清楚了Thrad1获取了xxxf60的锁正在等待xxxf10的锁,而Thread0正好相反
四、避免死锁
1、synchronized内置锁解决死锁
避免死锁归根结底就是保证获取锁顺序的一致性,静态的死锁比较容易避免,那么我们来看看上面转账导致导致的动态死锁如何处理。
思路:要保证获取锁顺序的一致性,我们可以从思考如何判断两个锁的不同,比如比较两个锁的hash,还有定义唯一锁的id甚至将锁实现Comparable都行,然后可以在代码中通过比较的不同来定义锁的顺序,比如获取锁总是先获取hashcode值小的,或者id小的都行,下面就是通过hash值来实现获取锁的顺序的一致性代码
SafeDynamicDeadLockTransfer .java
package com.concurrent.dynamicdeadlock.service;
import com.concurrent.dynamicdeadlock.UserAccount;
public class SafeDynamicDeadLockTransfer implements ITransfer{
private Object lock = new Object();
@Override
public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
//用hashcode对比from和to,始终将hashcode值小的先获取
//当然也可以再userAccount中定义一个唯一的id,然后通过id去比较,这样更简单
int fromHashCode = from.hashCode();
int toHashCode = to.hashCode();
if (fromHashCode < toHashCode) {
synchronized (from) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+" get lock:"+from.getName()+"...");
synchronized (to) {
from.flyMoney(amount);
to.addMoney(amount);
System.out.println(Thread.currentThread().getName()+" get lock:"+to.getName()+" end");
System.out.println("transfer success amount = "+amount);
}
}
}else if(toHashCode<fromHashCode) {
synchronized (to) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+" get lock:"+from.getName()+"...");
synchronized (from) {
from.flyMoney(amount);
to.addMoney(amount);
System.out.println(Thread.currentThread().getName()+" get lock:"+to.getName()+" end");
System.out.println("transfer success amount = "+amount);
}
}
}else {//hash冲突,重新争取一个锁,效率非常低,但是出现的概率极低,所以这个逻辑运行的可能性非常小,但是还是要处理
synchronized (lock) {
synchronized (from) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+" get lock:"+from.getName()+"...");
synchronized (to) {
from.flyMoney(amount);
to.addMoney(amount);
System.out.println(Thread.currentThread().getName()+" get lock:"+to.getName()+" end");
System.out.println("transfer success amount = "+amount);
}
}
}
}
}
}
测试代码中只需要改动一条代码
//ITransfer transfer = new DynamicDeadLockTransfer();
ITransfer transfer = new SafeDynamicDeadLockTransfer();
测试结果:
这就很自然的解决了上述死锁的问题,那么我们再思考下是否还有其他方式解决呢?我们思考一下显示锁
2、显示锁ReentrantLock来解决死锁
利用显示锁的tryLock来解决避免死锁,代码如下
SafeDynamicDeadLockTransferToo .java
package com.concurrent.dynamicdeadlock.service;
import java.util.Random;
import com.concurrent.dynamicdeadlock.UserAccount;
/**
* @author hongtaolong
* 使用显示锁来避免死锁
*/
public class SafeDynamicDeadLockTransferToo implements ITransfer {
@Override
public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
Random random = new Random();
while(true) {
if (from.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName()+" get lock:"+from.getLock()+"...");
if(to.getLock().tryLock()) {
try {
from.flyMoney(amount);
to.addMoney(amount);
System.out.println(Thread.currentThread().getName()+" get lock:"+to.getLock()+" end");
System.out.println("transfer success amount = "+amount);
break;
} finally {
to.getLock().unlock();
}
}
} finally {
from.getLock().unlock();
}
}
Thread.sleep(random.nextInt(10));//避免死锁影响效率
}
}
}
测试代码换成这个
//ITransfer transfer = new SafeDynamicDeadLockTransfer();
ITransfer transfer = new SafeDynamicDeadLockTransferToo();
运行结果:
仔细看上面的输出结果,首先程序肯定是没问题的,但是从上面看出并不是一次就成功了,而是尝试了两三次,这是为什么呢?这就要简单的介绍下活锁,死锁是不好的,我们应该杜绝编写死锁的程序,但是活锁也应该尽量避免。
活锁:尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生拿锁,释放锁的过程。
解决办法:每个线程休眠随机数,错开拿锁的时间。
上述这句代码就是解决活锁的效率问题
Thread.sleep(random.nextInt(10));//避免死锁影响效率
把这段代码注释一下,看看打印的结果:
上面的打印结果可以看出,由于相互谦让,导致拿锁的次数太多了,非常影响效率
活锁也用一个例子来解释一下,同样是上面A,B抢一双筷子,A,B一人抢到一只,但是这个时候两个人都太客气了,都放下手上的筷子让对方拿,这就导致一直持续这个动作,很久才能一个人完整的拿到一双筷子。
好了,分享就到这里为止了,如有问题,欢迎指正!谢谢!
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/111188.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...