15-修复死锁策略

nobility 发布于 2021-05-31 1147 次阅读


修复死锁策略

避免策略

银行转账

我们并不在乎获取锁的顺序,所以要避免相反的获取锁的顺序即可避免死锁,将transferMoney()方法做以下修改

public void transferMoney(int amount) {
  int fromHash = from.hashCode();
  int toHash = to.hashCode();
  if (fromHash > toHash) {  //保证互相转账时获取锁的顺序相同
    firstFromSecondTo(amount);
  } else if (fromHash < toHash) {
    firstToSecondFrom(amount);
  } else {
    synchronized (TransferMoney.class) {  //发生hash碰撞时,再次竞争一个全局锁,谁先抢到谁先执行
      firstFromSecondTo(amount);
    }
  }
}

private void firstFromSecondTo(int amount) {  //先from后to
  synchronized (from) {
    int balance = from.getBalance();  //查看余额是否充足
    if (balance - amount < 0) {
      System.out.println("余额不足,转账失败");
      return;
    }
    if (time > 0) {  //设置时间就延时,若设置转账延时就会进入死锁状态
      try {
        Thread.sleep(time);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    synchronized (to) {
      from.setBalance(balance - amount);  //转账操作
      to.setBalance(to.getBalance() + amount);
      System.out.println("从" + from.getName() + "到" + to.getName() + ",成功转账" + amount + "元");
    }
  }
}

private void firstToSecondFrom(int amount) {  //先to后from
  synchronized (to) {
    int balance = from.getBalance();  //查看余额是否充足
    if (balance - amount < 0) {
      System.out.println("余额不足,转账失败");
      return;
    }
    if (time > 0) {  //设置时间就延时,若设置转账延时就会进入死锁状态
      try {
        Thread.sleep(time);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    synchronized (from) {
      from.setBalance(balance - amount);  //转账操作
      to.setBalance(to.getBalance() + amount);
      System.out.println("从" + from.getName() + "到" + to.getName() + ",成功转账" + amount + "元");
    }
  }
}

哲学家就餐

  • 服务员检查:当哲学家在进行拿筷子前就会检查,当拿起筷子就会陷入死锁状态,就不会拿起筷子,从而避免死锁
  • 餐票检查:设置小于筷子数量的餐票,在哲学家进餐之前必须要拿到餐票才能进食,未拿到餐票的哲学家只能等待
  • 环路破坏:我们并不在乎获取筷子(锁)的顺序,所以要改变一个哲学家获取筷子(锁)的顺序,将initPhilosophers()方法做以下修改
public static Philosopher[] initPhilosophers(Object[] chopsticks) {
  Philosopher[] philosophers = new Philosopher[chopsticks.length];
  for (int i = 0; i < philosophers.length; i++) {  //初始化哲学家
    Object leftChopstick = chopsticks[i];
    Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];  //使用取余方式取到哲学家两测的筷子

    /*下面是修改代码*/
    if (i == philosophers.length - 1) {  //若是最后一个哲学家就将其拿筷子顺序颠倒
      philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
    } else {
      philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
    }
    /*上面是修改代码*/
  }
  return philosophers;
}

检测与恢复策略

一段时间检测是否有死锁发生,如果使用以下两种恢复策略

  • 进程或线程终止:逐个将线程终止,直到解除死锁,终止顺序最好采用以下几种规则
    • 重要程度:按照线程对于程序的重要性的优先级顺序
    • 已占用资源:占用资源越少的线程优先终止
    • 已运行时间:运行时间越短的线程优先终止
  • 资源抢占:将已发放出去的资源在回收回来,让线程回退几步,来解除死锁,成本较低
    • 缺点:可能同一个线程一直被抢占,造成该线程一直无法执行,导致饥饿现象

鸵鸟策略

若发生死锁的概率极低直接忽略,直到死锁发生时,再进行人工修复

实际开发中避免死锁

  • 尽量使用同步代码块,可自己指定锁对象
  • 尽量降低锁的粒度,使用不同的锁
  • 尽量专锁专用,避免不同功能使用同一把锁
  • 避免锁的嵌套,一旦获取锁的顺序相反,就会造成死锁
  • 放出资源前先查看是否能收回来,比如银行家算法
  • 为每个线程起一个有意义的名字,方便debug进行死锁排查
  • 使用并发类而不是自己设计锁:ConcurrentHashMapConcurrentLinkedQueuejava.util.concurrent.atomic.*
    • 多用并发集合,效率高,比如ConcurrentHashMap;少用同步集合,效率低,比如Hashtable
    • 原子类不仅简单方便,且效率比Lock更高
  • 设置超时时间:使用LocktryLock()方法,若拿不到锁就会等待指定时间,超时后就会返回false
public static void main(String[] args) throws InterruptedException {
  Lock lock1 = new ReentrantLock();
  Lock lock2 = new ReentrantLock();
  new Thread(() -> {
    try {
      while (true) {
        if (lock1.tryLock(1, TimeUnit.SECONDS)) {
          System.out.println(Thread.currentThread().getName() + "获得了lock1");
          Thread.sleep((long) (Math.random() * 10));  //随机延时
          if (lock2.tryLock(1, TimeUnit.SECONDS)) {
            System.out.println(Thread.currentThread().getName() + "获得了lock2");
            System.out.println(Thread.currentThread().getName() + "同时获取的了两把锁");
            lock1.unlock();  //完成操作后释放同时获取到的两个锁
            lock2.unlock();
            break;
          } else {
            System.out.println(Thread.currentThread().getName() + "获取lock2失败");
            System.out.println(Thread.currentThread().getName() + "释放lock1,并重新获取");
            lock1.unlock();  //无法同时获取两把锁,就释放lock1
          }
        } else {
          System.out.println(Thread.currentThread().getName() + "获取lock1失败");
        }
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }).start();
  new Thread(() -> {
    try {
      while (true) {
        if (lock2.tryLock(2, TimeUnit.SECONDS)) {
          System.out.println(Thread.currentThread().getName() + "获得了lock2");
          Thread.sleep((long) (Math.random() * 10));  //随机延时
          if (lock1.tryLock(2, TimeUnit.SECONDS)) {
            System.out.println(Thread.currentThread().getName() + "获得了lock1");
            System.out.println(Thread.currentThread().getName() + "同时获取的了两把锁");
            lock1.unlock();  //完成操作后释放同时获取到的两个锁
            lock2.unlock();
            break;
          } else {
            System.out.println(Thread.currentThread().getName() + "获取lock1失败");
            System.out.println(Thread.currentThread().getName() + "释放lock2,并重新获取");
            lock2.unlock();  //无法同时获取两把锁,就释放lock2
          }
        } else {
          System.out.println(Thread.currentThread().getName() + "获取lock1失败");
        }
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }).start();
}
此作者没有提供个人介绍
最后更新于 2021-05-31