解决多线程业务下 死锁和活锁的问题

关于死锁和活锁的概念

死锁:

是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

产生死锁的必要条件:
互斥条件:所谓互斥就是进程在某一时间内独占资源。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

活锁:

任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

活锁和死锁的区别

在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

业务场景

我们模拟最常见的转账业务: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));
		}
	}
}

执行结果:正常!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值