一、死锁的形成
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
实际工作中,我们经常听到死锁,死锁到底是什么东西呢?那么接下来就用实际例子演示一下银行转账功能。
用户转账的实体类:
/**
* @Author nanjunkai
* @Description: 用户转账的实体类
* @Date 2020/12/1 18:08:10
*/
public class UserAccount {
private final String name;//账户名称
private int money;//账户余额
// 显示锁
private Lock lock = new ReentrantLock();
public Lock getLock(){
return lock;
}
public UserAccount(String name, int money) {
this.name = name;
this.money = money;
}
public String getName() {
return name;
}
public int getMoney() {
return money;
}
// 转入金额
public void addMoney(int money){
this.money = money+money;
}
// 转出金额
public void flyMoney(int money){
this.money = money-money;
}
}
银行转账接口:
public interface ITransfer {
/**
* @description: TODO
* @date: 2020/12/1 18:39:30
* @param from 转出账户
* @param to 转入账户
* @param money 转账金额
* @return void
* @throws InterruptedException
*/
void transfer(UserAccount from, UserAccount to, int money) throws InterruptedException;
}
实现类:首先演示理想状态下,我们先锁转入账户,然后再锁转出账户
public class TransferAccount implements ITransfer {
/**
* @param from 转出账户
* @param to 转入账户
* @param money 转账金额
* @return void
* @description: 转账
* @date: 2020/12/1 18:39:30
*/
@Override
public void transfer(UserAccount from, UserAccount to, int money) throws InterruptedException {
synchronized (from) {//先锁转出
System.out.println(Thread.currentThread().getName());
Thread.sleep(100);
synchronized (to) {//再锁转入
System.out.println(Thread.currentThread().getName());
from.flyMoney(money);
to.addMoney(money);
}
}
}
}
模拟银行转账的功能:
public class PayCompany {
// 执行转账动作
private static class TransferThread extends Thread {
private String name;// 线程名字
private UserAccount from;// 转出账户
private UserAccount to;// 转入账户
private int money;// 转账金额
private ITransfer iTransfer;// 实际转账动作
public TransferThread(String name, UserAccount from, UserAccount to, int money, ITransfer iTransfer) {
this.name = name;
this.from = from;
this.to = to;
this.money = money;
this.iTransfer = iTransfer;
}
@Override
public void run() {
Thread.currentThread().setName(name);
try {
iTransfer.transfer(from, to, money);//转账操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
UserAccount zhangsan = new UserAccount("张三", 1000);
UserAccount lisi = new UserAccount("李四", 2000);
ITransfer iTransfer = new TransferAccount();
TransferThread zhangsantolisi = new TransferThread("张三转李四", zhangsan, lisi, 200, iTransfer);
TransferThread lisitozhangsan = new TransferThread("李四转张三", lisi, zhangsan, 200, iTransfer);
zhangsantolisi.start();
lisitozhangsan.start();
}
}
运行结果:只有两条结果,并且程序没有结束(死锁)
李四转张三
张三转李四
按代码逻辑看,应该是四条,一个转账有两条结果(一个转出一个转入)。但是实际只有两条,所以考虑到是死锁的原因,先看看死锁的持久情况。
1.先输入jps
2.输入jstack id
结果发现真的是产生了死锁。发生死锁的原因:当张三转账给李四时,张三锁住了自己,准备去拿李四的锁,但是刚好李四拿到自己的锁,也准备去拿张三的锁,结果到最后谁都拿不到锁。
二、解决死锁的方法
1.通过hash值确实账户的顺序。(上面实例死锁原因还有一点,动态顺序死锁,在实现时按照某种顺序加锁了,但是因为外部调用的问题,导致无法保证加锁顺序而产生的。)
重新写一个实现类:identityHashCode(返回对象的hashCode)
public class SafeOperate implements ITransfer {
private static Object tieLocK = new Object();//如果hash值相等,直接锁该对象
@Override
public void transfer(UserAccount from, UserAccount to, int money) throws InterruptedException {
int fromHash = System.identityHashCode(from);//返回对象的hashCode
int toHash = System.identityHashCode(to);
if (fromHash < toHash) {
synchronized (from) {//先锁转出
System.out.println(Thread.currentThread().getName());
Thread.sleep(100);
synchronized (to) {//再锁转入
System.out.println(Thread.currentThread().getName());
from.flyMoney(money);
to.addMoney(money);
}
}
} else if (fromHash > toHash) {
synchronized (to) {//先锁转出
System.out.println(Thread.currentThread().getName());
Thread.sleep(100);
synchronized (from) {//再锁转入
System.out.println(Thread.currentThread().getName());
from.flyMoney(money);
to.addMoney(money);
}
}
} else {
synchronized (tieLocK) {
synchronized (from) {//先锁转出
System.out.println(Thread.currentThread().getName());
Thread.sleep(100);
synchronized (to) {//再锁转入
System.out.println(Thread.currentThread().getName());
from.flyMoney(money);
to.addMoney(money);
}
}
}
}
}
}
main方法接口实现类修改,其他不变:
ITransfer iTransfer = new SafeOperate();
运行结果:
张三转李四
张三转李四
李四转张三
李四转张三
结果正常,也是线程安全的。
2.通过tryLock解决死锁
实现类:
public class SafeOperateToo implements ITransfer {
@Override
public void transfer(UserAccount from, UserAccount to, int money) throws InterruptedException {
long start = System.currentTimeMillis();
Random random = new Random();
while (true) {
if (from.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName());
if (to.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName());
from.flyMoney(money);
to.addMoney(money);
break;
} finally {
to.getLock().unlock();
}
}
} finally {
from.getLock().unlock();
}
}
Thread.sleep(random.nextInt(10));//可能会产生活锁
}
System.out.println(System.currentTimeMillis() - start);
}
}
main方法接口实现类修改,其他不变:
ITransfer iTransfer = new SafeOperateToo();
先把休眠代码注释运行,看下结果:
张三转李四
李四转张三
李四转张三
李四转张三
张三转李四
.........
李四转张三
304
张三转李四
张三转李四
305
其实只有两个人互相在转账,但是打印出了非常多的重复操作。为什么会出现这种情况?
三、活锁
活锁:当两者有一者拿到锁时,另一个同时也拿到锁时,就会造成谦让,从而导致尝试、失败....。
如何优化活锁?把休眠的代码打开,运行结果:
李四转张三
张三转李四
张三转李四
0
李四转张三
李四转张三
3
显而易见,尝试失败次数几乎没有了,时间也提升了很多倍。
四、总结
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
活锁:当两者有一者拿到锁时,另一个同时也拿到锁时,就会造成谦让,从而导致尝试、失败....
优化活锁:可以休眠一定时间来减少尝试和失败的次数。
死锁和活锁的区别?
死锁无法自行解开,而活锁可能自行解开。