当前位置:   article > 正文

解决多线程业务下 死锁和活锁的问题_活锁如何像死锁转化

活锁如何像死锁转化

关于死锁和活锁的概念

死锁:

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

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

活锁:

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

活锁和死锁的区别

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

业务场景

我们模拟最常见的转账业务:A账户给B账户转账的同时, B账户又在给A账户转账。此时会发生死锁。

模拟死锁开始准备

新建一个账户类

  1. /**
  2. * 用户账户类
  3. */
  4. public class UserAccount {
  5. //账户id(唯一的值)可能是银行卡号
  6. private final String id;
  7. //账户余额
  8. private Double money;
  9. public UserAccount(String id, Double amount) {
  10. this.id = id;
  11. this.money = amount;
  12. }
  13. public String getId() {
  14. return id;
  15. }
  16. public Double getMoney() {
  17. return money;
  18. }
  19. @Override
  20. public String toString() {
  21. return "UserAccount [id=" + id + ", money=" + money + "]";
  22. }
  23. /**
  24. * 转入资金
  25. * @param amout 金额
  26. */
  27. public void addMoney(double amout) {
  28. money = money + amout;
  29. System.out.println("账户:" + id + " ,转入"+ amout + "元,当前账户余额:" + money);
  30. }
  31. /** 转出资金
  32. * @param amout 金额
  33. */
  34. public void flyMoney(double amout) {
  35. if ((money - amout) >0) {
  36. money = money - amout;
  37. System.out.println("账户:" + id + " ,转出"+ amout + "元,当前账户余额:" + money);
  38. }else {
  39. System.out.println("账户余额不足!当前余额:" + money);
  40. }
  41. }
  42. }

定义一个交易的接口

  1. /**
  2. * 转账动作接口
  3. */
  4. public interface ITransfer {
  5. /**
  6. *
  7. * @param from 转出账户
  8. * @param to 转入账户
  9. * @param amount 转账金额
  10. * @throws InterruptedException
  11. */
  12. void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException;
  13. }

创建一个执行交易任务的线程类

  1. // 执行交易的线程任务
  2. private static class TransferThread extends Thread{
  3. // 线程名称
  4. private String name;
  5. // 转出账户
  6. private UserAccount from;
  7. // 转入账户
  8. private UserAccount to;
  9. // 交易金额
  10. private Double amount;
  11. // 交易方式
  12. private ITransfer transfer;
  13. public TransferThread(String name, UserAccount from, UserAccount to, Double amount, ITransfer transfer) {
  14. super();
  15. this.name = name;
  16. this.from = from;
  17. this.to = to;
  18. this.amount = amount;
  19. this.transfer = transfer;
  20. }
  21. @Override
  22. public void run() {
  23. Thread.currentThread().setName(name);
  24. try {
  25. transfer.transfer(from,to,amount);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. }

模拟一个不安全的交易

  1. /**
  2. * 不安全的交易方式实现
  3. * @author James Lee
  4. *
  5. */
  6. public class UnSafeTransfer implements ITransfer{
  7. @Override
  8. public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
  9. // 锁定转出账户
  10. synchronized (from) {
  11. System.out.println(Thread.currentThread().getName()+" 拿到【 " + from.getId() + "】账户的执行权!" );
  12. SleepTools.ms(1000);
  13. // 再锁定转入账户
  14. synchronized (to) {
  15. System.out.println(Thread.currentThread().getName()+" 拿到【 " + to.getId() + "】账户的执行权!" );
  16. // 这里代表两个账户的锁都拿到了才可以实现交易
  17. from.flyMoney(amount);
  18. to.addMoney(amount);
  19. }
  20. }
  21. }
  22. }
  1. public static void main(String[] args) {
  2. UserAccount james = new UserAccount("622...181: james", 100.00);
  3. UserAccount kobe = new UserAccount("622...182: kobe", 100.00);
  4. ITransfer transfer = new UnSafeTransfer();
  5. // 模拟 james给kobe转账20块钱
  6. TransferThread transferThread = new TransferThread("交易线程一", james, kobe, 20.00, transfer);
  7. // 模拟 kobe给james转账50块钱
  8. TransferThread transferThread2 = new TransferThread("交易线程二", kobe, james, 50.00, transfer);
  9. transferThread.start();
  10. transferThread2.start();
  11. }

测试结果:

发生的原因:

两个交易线程一直在等对方释放锁。

解决思路

解决死锁的思路:就是保证线程的顺序性。

有了思路以后,我们可以再在程序里控制锁账户的顺序性。

死锁解决办法一

根据实体的hashCode来判定,hashCode低的先锁定,高的后锁定,使用synchronized 加锁

代码实现:

  1. import xiangxue.day09.bank.UserAccount;
  2. import xiangxue.tools.SleepTools;
  3. /**
  4. * 不会产生死锁的安全转账: 基于hashCode或者实体的唯一主键
  5. *
  6. */
  7. public class SafeTransferByHashCode implements ITransfer {
  8. private static Object tieLock = new Object();//加时赛锁
  9. @Override
  10. public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
  11. // 首先获取实体账户的hashcode,考虑有可能传入之前会重写hashcode,所以我们调用jdk的identityHashCode获取原生值
  12. int fromHash = System.identityHashCode(from);
  13. int toHash = System.identityHashCode(to);
  14. // 先锁hash值小的账户
  15. if (fromHash < toHash) {
  16. synchronized (from) {
  17. System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】账户的执行权!");
  18. SleepTools.ms(1000);
  19. // 再锁定转入账户
  20. synchronized (to) {
  21. System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】账户的执行权!");
  22. // 这里代表两个账户的锁都拿到了才可以实现交易
  23. from.flyMoney(amount);
  24. to.addMoney(amount);
  25. }
  26. }
  27. }
  28. else if (toHash < fromHash) {
  29. synchronized (to) {
  30. System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】账户的执行权!");
  31. SleepTools.ms(1000);
  32. // 再锁定转入账户
  33. synchronized (from) {
  34. System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】账户的执行权!");
  35. // 这里代表两个账户的锁都拿到了才可以实现交易
  36. from.flyMoney(amount);
  37. to.addMoney(amount);
  38. }
  39. }
  40. }
  41. // 考虑到有hash冲突,这里在构造一把锁,让线程抢占,谁先抢占到,就执行。类似于篮球比赛,打成平手后的加时赛
  42. // hash冲突的概率是千万分之一,锁三次,对整体的性能其实影响不大
  43. else {
  44. synchronized (tieLock) {
  45. // 这里先锁哪个账户的顺序已经不重要了
  46. synchronized (from) {
  47. System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】账户的执行权!");
  48. synchronized (to) {
  49. System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】账户的执行权!");
  50. // 这里代表两个账户的锁都拿到了才可以实现交易
  51. from.flyMoney(amount);
  52. to.addMoney(amount);
  53. }
  54. }
  55. }
  56. }
  57. }
  58. }
  1. public static void main(String[] args) {
  2. PayCompany payCompany = new PayCompany();
  3. UserAccount james = new UserAccount("622...181: james", 100.00);
  4. UserAccount kobe = new UserAccount("622...182: kobe", 100.00);
  5. ITransfer transfer = new SafeTransferByHashCode();
  6. // 模拟 james给kobe转账20块钱
  7. TransferThread transferThread = new TransferThread("交易线程一", james, kobe, 20.00, transfer);
  8. // 模拟 kobe给james转账50块钱
  9. TransferThread transferThread2 = new TransferThread("交易线程二", kobe, james, 50.00, transfer);
  10. transferThread.start();
  11. transferThread2.start();
  12. }

测试结果:正常交易了!

代码分析

简洁明了,容易理解,不足是代码太多了,可能初中级java开发会这样。sync是独占锁,了解JUC编程的可能还知道一种可重入锁,Lock。

死锁解决办法二

使用lock 尝试性的拿锁

我们在用户账户类UserAccount.java 上加上以下代码:

  1. //显示锁
  2. private final Lock lock = new ReentrantLock();
  3. public Lock getLock() {
  4. return lock;
  5. }
  1. import xiangxue.day09.bank.UserAccount;
  2. /**
  3. * 不会产生死锁的安全转账: 使用可重入锁
  4. */
  5. public class SafeTransferByLock implements ITransfer{
  6. @Override
  7. public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
  8. while (true) {
  9. // 尝试拿到转出账户的锁
  10. if (from.getLock().tryLock()) {
  11. try {
  12. System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】账户的执行权!");
  13. // 再尝试拿转入账户的锁
  14. if (to.getLock().tryLock()) {
  15. try {
  16. System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】账户的执行权!");
  17. // 这里代表两个账户的锁都拿到了才可以实现交易
  18. from.flyMoney(amount);
  19. to.addMoney(amount);
  20. break;
  21. } finally {
  22. // 释放转入账户的锁
  23. to.getLock().unlock();
  24. }
  25. }
  26. } finally {
  27. // 释放转出账户的锁
  28. from.getLock().unlock();
  29. }
  30. }
  31. }
  32. }
  33. }
  1. public static void main(String[] args) {
  2. PayCompany payCompany = new PayCompany();
  3. UserAccount james = new UserAccount("622...181: james", 100.00);
  4. UserAccount kobe = new UserAccount("622...182: kobe", 100.00);
  5. ITransfer transfer = new SafeTransferByLock();
  6. // 模拟 james给kobe转账20块钱
  7. TransferThread transferThread = new TransferThread("交易线程一", james, kobe, 20.00, transfer);
  8. // 模拟 kobe给james转账50块钱
  9. TransferThread transferThread2 = new TransferThread("交易线程二", kobe, james, 50.00, transfer);
  10. transferThread.start();
  11. transferThread2.start();
  12. }

测试结果:发生了活锁

解决活锁的办法

在获取线程锁加上时间间隔,让程序休眠

改进SafeTransferByLock.java

  1. import java.util.Random;
  2. import xiangxue.day09.bank.UserAccount;
  3. import xiangxue.tools.SleepTools;
  4. /**
  5. * 不会产生死锁的安全转账: 使用可重入锁
  6. */
  7. public class SafeTransferByLock implements ITransfer{
  8. @Override
  9. public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
  10. Random r = new Random();
  11. while (true) {
  12. // 尝试拿到转出账户的锁
  13. if (from.getLock().tryLock()) {
  14. try {
  15. System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】账户的执行权!");
  16. // 再尝试拿转入账户的锁
  17. if (to.getLock().tryLock()) {
  18. try {
  19. System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】账户的执行权!");
  20. // 这里代表两个账户的锁都拿到了才可以实现交易
  21. from.flyMoney(amount);
  22. to.addMoney(amount);
  23. break;
  24. } finally {
  25. // 释放转入账户的锁
  26. to.getLock().unlock();
  27. }
  28. }
  29. } finally {
  30. // 释放转出账户的锁
  31. from.getLock().unlock();
  32. }
  33. }
  34. // 休眠。解决活锁
  35. SleepTools.ms(r.nextInt(10));
  36. }
  37. }
  38. }

执行结果:正常!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/721079
推荐阅读
相关标签
  

闽ICP备14008679号