死锁
当一个线程永远的持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞
eg: 1.线程A持有锁L并想获得锁M,同时线程B持有锁M并尝试获取锁L,那么这两个线程将永远地等待下去,这种情况就是最简单的死锁形式,称为抱死(Deadly Embrace); 2.多个线程由于存在环路的锁依赖关系而永远等待下去(循环等待)
锁顺序死锁
多个线程相互占用对方的资源的锁,而又相互等待对方释放锁,此时若无外力干预,这些线程将一直阻塞,形成死锁
两个线程试图以不同的顺序来获得相同的锁,即上面抱死形式
所有线程以固定的顺序来获得锁,就不会出现锁顺序死锁
//简单的锁顺序死锁 class LeftRightDeadLock{ private final Object left = new Object(); private final Object right = new Object(); public void leftRight(){ //获取锁顺序 left -> right synchronized (left){ synchronized (right){ doSomething(); } } } public void rightLeft(){ //获取锁顺序 right -> left synchronized (right){ synchronized (left){ doSomething(); } } } public void doSomething(){} }
动态锁顺序死锁
//动态的锁顺序死锁(A->B转账,同时B->转账容易死锁(抱死)) public void transferMoney(Account fromAccount,Account toAccount,DollarAmount amount){ synchronized(fromAccount){ synchronized(toAccount){ //转账操作 } } }
//通过锁顺序来避免死锁 private static final Object tieLock = new Object();//额外添加一个锁,只有当获取到这个锁时才能获取其他锁(等同于只有一个锁,极少数情况使用,会影响并发性能) public void transferMoney(final Account fromAcct,final Account toAcct,final DollarAmount amount){ int fromHash = System.identityHashCode(fromAcct); int toHash = System.identityHashCode(toAcct); if(fromHash < toHash){//加锁顺序(hash: 小->大) synchronized(fromAcct){ synchronized(toAcct){ //转账操作 } } } else if(fromHash > toHash){//加锁顺序(hash: 小->大) synchronized(toAcct){ synchronized(fromAcct){ //转账操作 } } } else{//hash冲突(极少数情况):额外增加一个锁,获取该锁后才能获取其他锁,等同于只有一个锁 synchronized(tieLock){ synchronized(fromAcct){ synchronized(toAcct){ //转账操作 } } } } }
** 在协作对象之间发生死锁**
//在相互协作对象之间的锁顺序死锁 //并行的调用setLocation与getImage方法可能会导致锁顺序死锁 class Taxi{ //...其他内容 public synchronized Point getLocation(){ return location; } public synchronized void setLocation(Point location){ this.location = location; if(location.equals(destination)) dispatcher.notifyAvailable(this);//调用Dispatcher类方法(隐式的获取另一个锁) } } class Dispatcher{ //...其他内容 public synchronized void notifyAvailable(Taxi taxi){ //更新 } public synchronized Image getImage(){ Image image = new Image(); for(Taxi t : taxis) //隐式获取另一个锁 image.drawMarker(t.getLocation); return image; } }
开放调用
调用某个方法是不需要持有锁,这种调用被称为开放调用
在程序中应尽量使用开放调用,与那些持有锁时调用外部方法的程序相比,更易于对依赖于开放调用的程序进行死锁分析
//通过开放调用来避免在相互协作的对象之间产生死锁 class Taxi{ 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)//开放调用,调用时不需要持有锁,执行此方法只会在方法内部隐式的获取一个锁 } } class Dispatcher{ public synchronized void notifyAvailable(Taxi taxi){ avilableTaxis.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 } }
资源死锁
找出什么地方将获取多个锁,然后对所有这些实例进行全局分析,确保它们在整个过程中获取锁的顺序保持一致
尽可能的使用开放调用,这能极大地简化分析过
- 支持定时的锁
- 显式的使用Lock类中的定时tryLock功能,可以指定一个超时限制(Timeout),等待超时后会返回一个失败信息(对于嵌套的方法中调用请求多个锁,即使你知道已经持有外层锁,也无法释放它)
- 通过线程转储信息来分析死锁
- 饥饿
- 当线程由于无法访问它所需的资源而不能继续执行时,就发生了”饥饿(Starvaion)”
- 如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级的线程无法得到执行,这就是饥饿,还有一种情况是一个线程一直占着一个资源不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够执行的,如那个占用资源的线程结束了并且释放了资源
- 要避免使用线程优先级,因为会增加平台依赖性,并可能导致活跃性问题
- 糟糕的响应性
- CPU密集型的后台任务可能对响应性造成影响,他们会竞争CPU的时钟周期,可以降低它们的优先级
- 不良的锁管理也可能导致糟糕的响应性,如果某个线程长时间占有一个锁(正在对一个大容器迭代),而其他想要访问这个容器的线程就必须等待很长时间
- 活锁
- 线程不断重复执行相同的操作,而且总是失败,导致不能继续往后执行,如在处理事务消息的应用程序中:如果不能成功处理某个消息,那么将回滚事务,并将它重新放入队列开头,如果这个消息存在错误,那将一直循环下去,无法继续执行
- 拿到资源却有相互释放不执行,当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多线程之间跳动而又得不到执行
- 对于相互谦让的可以在重试机制中引入随机性,如两台机器尝试使用相同的载波发送数据包,那么这些数据包就会发生冲突,检测到冲突后会在稍后再次尝试发送,如果两者都选择1秒后重试,那么又会发生冲突,并且不断的冲突下去,可以通过设置随机的等待时间来规避这种情况