chapter10_避免活跃性危险_1_死锁

  • 死锁包括了几种情况, 例如__锁顺序死锁__、动态的锁顺序死锁协作对象间的死锁、__资源死锁__等几种情况

  • 锁顺序死锁

    (1) 两个线程以不同的顺序获得相同的锁, 就会引发锁顺序死锁

    示例

      public class LeftRightDeadlock {
    
          private final Object left = new Object();
          private final Object right = new Object();
    
          public void leftRight() {
              synchronized (left) {
                  synchronized (right) {
                      doSomething();
                  }
              }
          }
    
          public void rightLeft() {
              synchronized (right) {
                  synchronized (left) {
                      doSomethingElse();
                  }
              }
          }
    
          void doSomething() {
          }
    
          void doSomethingElse() {
          }
      }
    

    这个示例中, 可能会发生两个线程分别执行leftRight()和rightLeft(), 由于它们各持有一把锁, 因此会造成死锁

    (2) 如果所有线程以固定的顺序获得锁, 那么在程序中就不会出现锁顺序死锁的问题

  • 动态的锁顺序死锁

    (1) 看起来正常的代码, 由于参数调用顺序的不同, 可能会动态的引发锁顺序死锁

    示例

      public class DynamicOrderDeadlock {
    
          public static void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) throws InsufficientFundsException {
    
              synchronized (fromAccount) {
                  synchronized (toAccount) {
    
                      if (fromAccount.getBalance().compareTo(amount) < 0) {
                          throw new InsufficientFundsException("Dead Lock");
                      } else {
                          fromAccount.debit(amount);
                          toAccount.credit(amount);
                      }
                  }
              }
          }
    
          ...
      }
    

    这段代码中的锁是fromAccount和toAccount, 当调用时如果发生两个线程恰好使用了相同的两个账户, 但是顺序相反时, 就有可能造成死锁

    (2) 我们无法控制传入参数的顺序, 为了解决这一问题, 需要__定义锁的顺序__

    修改后代码

      public class InduceLockOrder {
    
          private static final Object tieLock = new Object();
    
          public void transferMoney(final Account fromAcct, final Account toAcct,
          final DollarAmount amount) throws InsufficientFundsException {
    
              class Helper {
    
                  public void transfer() throws InsufficientFundsException {
    
                      if (fromAcct.getBalance().compareTo(amount) < 0) {
                          throw new InsufficientFundsException();
                      } else {
                          fromAcct.debit(amount);
                          toAcct.credit(amount);
                      }
                  }
              }
    
              int fromHash = System.identityHashCode(fromAcct);
              int toHash = System.identityHashCode(toAcct);
    
              if (fromHash < toHash) {
                  synchronized (fromAcct) {
                      synchronized (toAcct) {
                          new Helper().transfer();
                      }
                  }
              } else if (fromHash > toHash) {
                  synchronized (toAcct) {
                      synchronized (fromAcct) {
                          new Helper().transfer();
                      }
                  }
              } else {
                  synchronized (tieLock) {
                      synchronized (fromAcct) {
                          synchronized (toAcct) {
                              new Helper().transfer();
                          }
                      }
                  }
              }
          }
    
          ...
      }
    

    修改后的代码通过调用System.identityHashCode()方法获得Object对象的原始hashCode值, 并且始终先获得hashCode小的对象的锁,这样就(貌似)避免了动态锁顺序死锁;

    但是有极小的概率出现__两个对象的hash值__相同, 为了避免这个问题引入了专门的锁对象tieLock用来控制只能让一个线程进入;

    其实引入tieLock可以解决一切问题, 那么为什么要大费周章的计算hashCode呢? 因为只有tieLock相当于对整个方法加锁, 此时会有性能上的缺陷。

  • 在协作对象之间发生的死锁

    (1) 这种死锁很难查找, 它发生的情况是获取多个锁的操作发生在不同的方法中(甚至有可能发生在协同的类中), 但是死锁的原因和__锁顺序死锁__一样, 也是获取锁的顺序不同

    示例

      public class CooperatingDeadlock {
    
          // Warning: deadlock-prone!
    
          class Taxi {
    
              @GuardedBy("this")
              private Point location, destination;
              private final Dispatcher dispatcher;
    
              public Taxi(Dispatcher dispatcher) {
    
                  this.dispatcher = dispatcher;
              }
    
              public synchronized Point getLocation() {
    
                  return location;
              }
    
              public synchronized void setLocation(Point location) {
    
                  this.location = location;
    
                  if (location.equals(destination)) {
                      dispatcher.notifyAvailable(this);
                  }
              }
    
              public synchronized Point getDestination() {
                  return destination;
              }
    
              public synchronized void setDestination(Point destination) {
                  this.destination = destination;
              }
          }
    
          class Dispatcher {
    
              @GuardedBy("this")
              private final Set<Taxi> taxis;
    
              @GuardedBy("this")
              private final Set<Taxi> availableTaxis;
    
              public Dispatcher() {
                  taxis = new HashSet<Taxi>();
                  availableTaxis = new HashSet<Taxi>();
              }
    
              public synchronized void notifyAvailable(Taxi taxi) {
    
                  availableTaxis.add(taxi);
              }
    
              public synchronized Image getImage() {
    
                  Image image = new Image();
    
                  for (Taxi t : taxis) {
                      image.drawMarker(t.getLocation());
                  }
    
                  return image;
              }
          }
    
          class Image {
              public void drawMarker(Point p) {
              }
          }
      }
    

    这个示例中, Taxi和Dispatcher是两个耦合的类, 一个代表出租车类(它和一个调度对象关联), 一个代表调度(它和一组出租车关联)。 发生死锁的情形可能是: 一个出租车对象想要setLocation, 那么它先持有自身的锁, 希望获得关联的Dispatcher的锁; 一个Dispatcher想要getImage, 那么它先持有自身的锁, 希望获得各个taxi的锁。 此时就出现了死锁, 只不过由于死锁发生在不同类的不同方法之间很难查找

    (3) 如果在持有锁的情况下调用某个外部方法, 那么就需要死锁。当在这个外部方法可能会获取其他锁时发生死锁,或者阻塞时间过长, 就会导致其他线程无法及时获得当前持有的锁, 引发活跃性问题

    (4) 解决的方式是__尽可能使用开放调用__

    开放调用: 如果在调用某个方法时不需要持有锁, 那么这种调用称为开放调用

    封装方便了线程安全性的分析, 那么开放调用就方便了程序活跃性的分析

    同步代码块应该仅被用于保护那些设计共享状态的操作, 收缩同步代码块的范围可以提高伸缩性

    4° 修改后的Taxi和Dispatcher示例(不再出现死锁)

      class CooperatingNoDeadlock {
    
          @ThreadSafe
          class Taxi {
    
              @GuardedBy("this")
              private Point location, destination;
              private final Dispatcher dispatcher;
    
              public Taxi(Dispatcher dispatcher) {
    
                  this.dispatcher = dispatcher;
              }
    
              public synchronized Point getLocation() {
    
                  return location;
              }
    
              public void setLocation(Point location) {
    
                  boolean reachedDestination;
    
                  synchronized (this) {
                      this.location = location;
                      reachedDestination = location.equals(destination);
                  }
    
                  if (reachedDestination) {
                      dispatcher.notifyAvailable(this);
                  }
              }
    
              public synchronized Point getDestination() {
    
                  return destination;
              }
    
              public synchronized void setDestination(Point destination) {
    
                  this.destination = destination;
              }
          }
    
          @ThreadSafe
          class Dispatcher {
    
              @GuardedBy("this")
              private final Set<Taxi> taxis;
      
              @GuardedBy("this")
              private final Set<Taxi> availableTaxis;
    
              public Dispatcher() {
                  taxis = new HashSet<Taxi>();
                  availableTaxis = new HashSet<Taxi>();
              }
    
              public synchronized void notifyAvailable(Taxi taxi) {
    
                  availableTaxis.add(taxi);
              }
    
              public Image getImage() {
    
                  Set<Taxi> copy;
                  synchronized (this) {
                      copy = new HashSet<Taxi>(taxis);
                  }
    
                  Image image = new Image();
                  for (Taxi t : copy) {
                      image.drawMarker(t.getLocation());
                  }
    
                  return image;
              }
          }
    
          class Image {
              public void drawMarker(Point p) {
              }
          }
      }
    

    修改后的示例没有将setLocation和getImage方法在整个方法体上同步, 而是采取了局部代码块同步, 这样不存在一个方法在持有锁的同时还请求锁的情况, 也就避免了死锁

  • 资源死锁

    (1) 之前的几种死锁情况是不同线程都在等待彼此拥有的锁, 资源死锁指的是不同线程在等待其他线程拥有的资源。

    例如数据库连接池中只有一个连接, 现在两个线程都需要与两个数据库连接, 一个线程占据着A数据库请求B数据库, 另一个线程占据着B数据库请求A数据库, 此时就会发生死锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值