关于死锁和活锁的概念
死锁:
是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
产生死锁的必要条件:
互斥条件:所谓互斥就是进程在某一时间内独占资源。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
活锁:
任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。
活锁和死锁的区别
在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
业务场景
我们模拟最常见的转账业务:A账户给B账户转账的同时, B账户又在给A账户转账。此时会发生死锁。
模拟死锁开始准备
新建一个账户类
/**
* 用户账户类
*/
public class UserAccount {
//账户id(唯一的值)可能是银行卡号
private final String id;
//账户余额
private Double money;
public UserAccount(String id, Double amount) {
this.id = id;
this.money = amount;
}
public String getId() {
return id;
}
public Double getMoney() {
return money;
}
@Override
public String toString() {
return "UserAccount [id=" + id + ", money=" + money + "]";
}
/**
* 转入资金
* @param amout 金额
*/
public void addMoney(double amout) {
money = money + amout;
System.out.println("账户:" + id + " ,转入"+ amout + "元,当前账户余额:" + money);
}
/** 转出资金
* @param amout 金额
*/
public void flyMoney(double amout) {
if ((money - amout) >0) {
money = money - amout;
System.out.println("账户:" + id + " ,转出"+ amout + "元,当前账户余额:" + money);
}else {
System.out.println("账户余额不足!当前余额:" + money);
}
}
}
定义一个交易的接口
/**
* 转账动作接口
*/
public interface ITransfer {
/**
*
* @param from 转出账户
* @param to 转入账户
* @param amount 转账金额
* @throws InterruptedException
*/
void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException;
}
创建一个执行交易任务的线程类
// 执行交易的线程任务
private static class TransferThread extends Thread{
// 线程名称
private String name;
// 转出账户
private UserAccount from;
// 转入账户
private UserAccount to;
// 交易金额
private Double amount;
// 交易方式
private ITransfer transfer;
public TransferThread(String name, UserAccount from, UserAccount to, Double amount, ITransfer transfer) {
super();
this.name = name;
this.from = from;
this.to = to;
this.amount = amount;
this.transfer = transfer;
}
@Override
public void run() {
Thread.currentThread().setName(name);
try {
transfer.transfer(from,to,amount);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
模拟一个不安全的交易
/**
* 不安全的交易方式实现
* @author James Lee
*
*/
public class UnSafeTransfer implements ITransfer{
@Override
public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
// 锁定转出账户
synchronized (from) {
System.out.println(Thread.currentThread().getName()+" 拿到【 " + from.getId() + "】账户的执行权!" );
SleepTools.ms(1000);
// 再锁定转入账户
synchronized (to) {
System.out.println(Thread.currentThread().getName()+" 拿到【 " + to.getId() + "】账户的执行权!" );
// 这里代表两个账户的锁都拿到了才可以实现交易
from.flyMoney(amount);
to.addMoney(amount);
}
}
}
}
public static void main(String[] args) {
UserAccount james = new UserAccount("622...181: james", 100.00);
UserAccount kobe = new UserAccount("622...182: kobe", 100.00);
ITransfer transfer = new UnSafeTransfer();
// 模拟 james给kobe转账20块钱
TransferThread transferThread = new TransferThread("交易线程一", james, kobe, 20.00, transfer);
// 模拟 kobe给james转账50块钱
TransferThread transferThread2 = new TransferThread("交易线程二", kobe, james, 50.00, transfer);
transferThread.start();
transferThread2.start();
}
测试结果:
发生的原因:
两个交易线程一直在等对方释放锁。
解决思路
解决死锁的思路:就是保证线程的顺序性。
有了思路以后,我们可以再在程序里控制锁账户的顺序性。
死锁解决办法一
根据实体的hashCode来判定,hashCode低的先锁定,高的后锁定,使用synchronized 加锁
代码实现:
import xiangxue.day09.bank.UserAccount;
import xiangxue.tools.SleepTools;
/**
* 不会产生死锁的安全转账: 基于hashCode或者实体的唯一主键
*
*/
public class SafeTransferByHashCode implements ITransfer {
private static Object tieLock = new Object();//加时赛锁
@Override
public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
// 首先获取实体账户的hashcode,考虑有可能传入之前会重写hashcode,所以我们调用jdk的identityHashCode获取原生值
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
// 先锁hash值小的账户
if (fromHash < toHash) {
synchronized (from) {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】账户的执行权!");
SleepTools.ms(1000);
// 再锁定转入账户
synchronized (to) {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】账户的执行权!");
// 这里代表两个账户的锁都拿到了才可以实现交易
from.flyMoney(amount);
to.addMoney(amount);
}
}
}
else if (toHash < fromHash) {
synchronized (to) {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】账户的执行权!");
SleepTools.ms(1000);
// 再锁定转入账户
synchronized (from) {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】账户的执行权!");
// 这里代表两个账户的锁都拿到了才可以实现交易
from.flyMoney(amount);
to.addMoney(amount);
}
}
}
// 考虑到有hash冲突,这里在构造一把锁,让线程抢占,谁先抢占到,就执行。类似于篮球比赛,打成平手后的加时赛
// hash冲突的概率是千万分之一,锁三次,对整体的性能其实影响不大
else {
synchronized (tieLock) {
// 这里先锁哪个账户的顺序已经不重要了
synchronized (from) {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】账户的执行权!");
synchronized (to) {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】账户的执行权!");
// 这里代表两个账户的锁都拿到了才可以实现交易
from.flyMoney(amount);
to.addMoney(amount);
}
}
}
}
}
}
public static void main(String[] args) {
PayCompany payCompany = new PayCompany();
UserAccount james = new UserAccount("622...181: james", 100.00);
UserAccount kobe = new UserAccount("622...182: kobe", 100.00);
ITransfer transfer = new SafeTransferByHashCode();
// 模拟 james给kobe转账20块钱
TransferThread transferThread = new TransferThread("交易线程一", james, kobe, 20.00, transfer);
// 模拟 kobe给james转账50块钱
TransferThread transferThread2 = new TransferThread("交易线程二", kobe, james, 50.00, transfer);
transferThread.start();
transferThread2.start();
}
测试结果:正常交易了!
代码分析
简洁明了,容易理解,不足是代码太多了,可能初中级java开发会这样。sync是独占锁,了解JUC编程的可能还知道一种可重入锁,Lock。
死锁解决办法二
使用lock 尝试性的拿锁
我们在用户账户类UserAccount.java 上加上以下代码:
//显示锁
private final Lock lock = new ReentrantLock();
public Lock getLock() {
return lock;
}
import xiangxue.day09.bank.UserAccount;
/**
* 不会产生死锁的安全转账: 使用可重入锁
*/
public class SafeTransferByLock implements ITransfer{
@Override
public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
while (true) {
// 尝试拿到转出账户的锁
if (from.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】账户的执行权!");
// 再尝试拿转入账户的锁
if (to.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】账户的执行权!");
// 这里代表两个账户的锁都拿到了才可以实现交易
from.flyMoney(amount);
to.addMoney(amount);
break;
} finally {
// 释放转入账户的锁
to.getLock().unlock();
}
}
} finally {
// 释放转出账户的锁
from.getLock().unlock();
}
}
}
}
}
public static void main(String[] args) {
PayCompany payCompany = new PayCompany();
UserAccount james = new UserAccount("622...181: james", 100.00);
UserAccount kobe = new UserAccount("622...182: kobe", 100.00);
ITransfer transfer = new SafeTransferByLock();
// 模拟 james给kobe转账20块钱
TransferThread transferThread = new TransferThread("交易线程一", james, kobe, 20.00, transfer);
// 模拟 kobe给james转账50块钱
TransferThread transferThread2 = new TransferThread("交易线程二", kobe, james, 50.00, transfer);
transferThread.start();
transferThread2.start();
}
测试结果:发生了活锁
解决活锁的办法
在获取线程锁加上时间间隔,让程序休眠
改进SafeTransferByLock.java
import java.util.Random;
import xiangxue.day09.bank.UserAccount;
import xiangxue.tools.SleepTools;
/**
* 不会产生死锁的安全转账: 使用可重入锁
*/
public class SafeTransferByLock implements ITransfer{
@Override
public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
Random r = new Random();
while (true) {
// 尝试拿到转出账户的锁
if (from.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】账户的执行权!");
// 再尝试拿转入账户的锁
if (to.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】账户的执行权!");
// 这里代表两个账户的锁都拿到了才可以实现交易
from.flyMoney(amount);
to.addMoney(amount);
break;
} finally {
// 释放转入账户的锁
to.getLock().unlock();
}
}
} finally {
// 释放转出账户的锁
from.getLock().unlock();
}
}
// 休眠。解决活锁
SleepTools.ms(r.nextInt(10));
}
}
}