《java并发编程实战》笔记(第10章)

来源:互联网 发布:勒夏特列原理 知乎 编辑:程序博客网 时间:2024/06/09 15:41

死锁
经典的“哲学家进餐”问题: 5个哲学家去吃中餐,坐在一张圆桌旁,他们有五根筷子(不是五双),并且每两个人中间放一根筷子。哲学家们时而思考,时而进餐。每个人都需要一双筷子才能吃到东西,并且在吃完后将筷子放回原处继续思考。
如果每个人都立即抓住自己左边的筷子,然后等待自己右边的筷子空出来,但同时又不放下已经拿到的筷子。——产生死锁
也就是 每个人都拥有其他人需要的资源,同时又等待其他人已经拥有的资源,并且每个人在获得所有需要的资源之前都不会放弃已经拥有的资源。
当一个线程永远地持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞。
在线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,那么这两个线程将永远地等待下去。这种情况就是最简单的死锁形式(或者称为“抱死【Deadly Embrace 】”)
如果所有线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题。

   //容易发生死锁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();                  }            }      }}
* 动态的锁顺序死锁 考虑下面看似无害的代码 ,将资金从一个账户转到另一个账户,转账前,先获取两个Account对象的锁。
 public void transferMoney(Account fromAccount ,                                            Account toAccount ,                                            int account)                       throws InsufficientResourcesException{            synchronized (fromAccount) {                  synchronized (toAccount) {                        if(fromAccount.getBalance()-account<0){                              throw new InsufficientResourcesException() ;                        }else{                              fromAccount.debit(account);                              toAccount.credit(account) ;                        }                  }            }      }

所有的线程看似都按照相同的顺序来获得锁,但事实上锁的顺序取决于传递给transferMoney的参数顺序,而这些参数顺序取决于外部输入。如果两个线程同时调用transferMoney ,其中一个从X向Y转账,一个从Y向X转账,那么就会发生死锁。

private static final Object tieLock = new Object();      public void transferMoney(final Account fromAccount ,                                          final  Account toAccount ,                                          final  int account)                        throws InsufficientResourcesException{            class Helper{                  public void transfer() throws InsufficientResourcesException{                        if(fromAccount.getBalance()-account<0){                              throw new InsufficientResourcesException() ;                        }else{                              fromAccount.debit(account);                              toAccount.credit(account) ;                        }                  }            }            /**             *  1.hashCode()方法是Object类下面的一个方法,供继承类重写,根据对象内存地址计算哈希值.                  2.String类重写了hashCode方法,并改为根据字符序列来计算哈希值.                  3.identityHashCode()方法是System类中的静态方法,根据对象内存地址来计算哈希值.             */            int fromHash = System.identityHashCode(fromAccount);  //System.identityHashCode返回Object.hashCode()的值            int toHash = System.identityHashCode(toAccount);            if(fromHash < toHash){                  synchronized (fromAccount) {                        synchronized (toAccount) {                              new Helper().transfer();                        }                  }            } else if(fromHash > toHash){                  synchronized (toAccount) {                        synchronized (fromAccount) {                              new Helper().transfer();                        }                  }            }else{                  synchronized (tieLock) {                        synchronized (fromAccount) {                              synchronized (toAccount) {                                    new Helper().transfer();                              }                        }                  }            }      }

通过System.identityHashCode 可以很好的解决问题(System.identityHashCode中出现散列冲突的频率非常低,如果经常出现散列冲突的情况,那么这个解决方案可能会成为并发性的瓶颈)

  • 在协作对象之间发生的死锁
    如果在持有锁时调用某个外部方法,那么将出现活跃性问题。在这个外部方法中可能会获取其他锁(可能会产生死锁),或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁。

  • 开放调用
    如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用(Open Call)
    通过尽可能地使用开放调用,将更易于找出那些需要获取多个锁的代码路径,因此也就更容易确保采用一致的顺序来获得锁。
    在程序中应尽量使用开放调用。与那些在持有锁时调用外部方法的程序相比,更容易对依赖于开放调用的程序进行死锁分析。

  • 死锁的避免与诊断
    在使用细粒度锁的程序中, 可以通过使用一种两阶段策略来检查代码中的死锁:首先,找出在什么地方将获取多个锁(使这个集合尽量小),然后对所有这些实例进行全局分析,从而确保它们在整个程序中获取锁的顺序都保持一致。尽可能地使用开放调用,这能极大地简化分析过程
    1. 支持定时的锁。 显式使用Lock类中的定时tryLock功能来代替内置锁机制。(参见第13章)
    使用内置锁时,只要没有获得锁,就会永远等待下去,而显式锁则可以指定一个超时时限Timeout,在等待超过该时间后tryLock会返回一个失败信息。
    2.通过线程转储信息来分析死锁
    JVM通过线程转储(Thread Dump)来帮忙识别死锁的发生。 (可使用一些工具 如eclipse的MemoryAnalyzer来分析DUMP文件)

  • 其他活跃性危险
    1.饥饿
    当线程由于无法访问它所需要的资源而不能继续执行时,就会发生饥饿“Starvation”。
    引发饥饿的最常见资源就是CPU时钟周期。(百度百科:CPU时钟周期通常为节拍脉冲或T周期,既主频的倒数,它是处理操作的最基本的单位。)
    通常,我们尽量不要改变线程的优先级。只要改变了线程的优先级,程序的行为就将与平台相关,并且会导致发生饥饿问题的风险。你经常能发现某个程序会在一些奇怪的地方调用Thread.sleep或Thread.yield,这是因为该程序试图客服优先级调整问题或响应性问题,并试图让低优先级的线程执行更多的时间。
    要避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题。在大多数并发应用程序中,都可以使用默认的线程优先级。
    2.糟糕的响应性
    不良的锁管理也可能导致糟糕的响应性。例如:某个线程长时间占用一个锁(比如对一个大容器进行迭代,并对每个元素进行计算密集的处理),那么其他想要访问这个容器的线程就必须等待很长时间
    3.活锁
    活锁通常发生在处理事务消息的应用程序中:如果不能成功的处理某个消息,那么消息处理机制将回滚整个事务,并将它重新排到队列的开头。由于这条消息每次都被放回到队列开头,因此处理器将被反复调用,并返回相同的结果。这种这种形式的活锁通常是由过度的错误恢复代码造成的。
    当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。就像
    要解决这种活锁问题,需要在重试机制中引入随机性。

原创粉丝点击