并发-避免活跃性问题


死锁

当一个线程永远的持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞

eg: 1.线程A持有锁L并想获得锁M,同时线程B持有锁M并尝试获取锁L,那么这两个线程将永远地等待下去,这种情况就是最简单的死锁形式,称为抱死(Deadly Embrace); 2.多个线程由于存在环路的锁依赖关系而永远等待下去(循环等待)

  1. 锁顺序死锁

    • 多个线程相互占用对方的资源的锁,而又相互等待对方释放锁,此时若无外力干预,这些线程将一直阻塞,形成死锁

    • 两个线程试图以不同的顺序来获得相同的锁,即上面抱死形式

    • 所有线程以固定的顺序来获得锁,就不会出现锁顺序死锁

      //简单的锁顺序死锁
      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(){}
      }
  2. 动态锁顺序死锁

     //动态的锁顺序死锁(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){
                         //转账操作
                     }
                 }
             }
         }
     }
  3. ** 在协作对象之间发生死锁**

     //在相互协作对象之间的锁顺序死锁
     //并行的调用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;
         }
     }
  4. 开放调用

    • 调用某个方法是不需要持有锁,这种调用被称为开放调用

    • 在程序中应尽量使用开放调用,与那些持有锁时调用外部方法的程序相比,更易于对依赖于开放调用的程序进行死锁分析

      //通过开放调用来避免在相互协作的对象之间产生死锁
      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
        }
      }
  5. 资源死锁

    • 多个线程在相同的资源集合上等待时,也会发生死锁
    • eg: 1.线程A持有数据库D1的连接,并等待与数据库D2的连接,线程B持有D2的链接并等待D1的链接2.一个任务提交另一个任务,并等待被提交任务在单线程的Executor中执行完成,这种情况下第一个任务将永远等待下去,并使得这个Executor中执行的所有其他任务都停止执行,有界线程池/资源池与相互依赖的任务不能一起使用

      死锁的避免与诊断

      如果一个程序每次至多只能获取一个锁,那么就不会产生锁顺序死锁

找出什么地方将获取多个锁,然后对所有这些实例进行全局分析,确保它们在整个过程中获取锁的顺序保持一致

尽可能的使用开放调用,这能极大地简化分析过

  1. 支持定时的锁
    • 显式的使用Lock类中的定时tryLock功能,可以指定一个超时限制(Timeout),等待超时后会返回一个失败信息(对于嵌套的方法中调用请求多个锁,即使你知道已经持有外层锁,也无法释放它)
  2. 通过线程转储信息来分析死锁
    • JVM通过线程转储(Thread Dump)来帮助识别死锁的发生,线程转储包括各个运行中的线程的栈追踪信息,这类似于发生异常时的栈追踪信息,线程转储还包含加锁信息,例如每个线程持有哪些锁,在哪个栈帧中获得这些锁,以及被阻塞的线程正在等待获取哪一个锁
    • 在生成线程转储之前,JVM将再等待关系图中通过搜索循环来找出死锁,如果发现了一个死锁,则获取相应的死锁信息,例如在死锁中涉及哪些锁和线程,以及这个锁的获取操作位于程序的哪些位置
    • UNIX平台触发线程转储操作,可以通过向JVM的进程发送SIGOUIT信号(kill -3),或者按ctrl-,在Windows下按ctrl-break键

      其他活跃性危险

  3. 饥饿
    • 当线程由于无法访问它所需的资源而不能继续执行时,就发生了”饥饿(Starvaion)”
    • 如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级的线程无法得到执行,这就是饥饿,还有一种情况是一个线程一直占着一个资源不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够执行的,如那个占用资源的线程结束了并且释放了资源
    • 要避免使用线程优先级,因为会增加平台依赖性,并可能导致活跃性问题
  4. 糟糕的响应性
    • CPU密集型的后台任务可能对响应性造成影响,他们会竞争CPU的时钟周期,可以降低它们的优先级
    • 不良的锁管理也可能导致糟糕的响应性,如果某个线程长时间占有一个锁(正在对一个大容器迭代),而其他想要访问这个容器的线程就必须等待很长时间
  5. 活锁
    • 线程不断重复执行相同的操作,而且总是失败,导致不能继续往后执行,如在处理事务消息的应用程序中:如果不能成功处理某个消息,那么将回滚事务,并将它重新放入队列开头,如果这个消息存在错误,那将一直循环下去,无法继续执行
    • 拿到资源却有相互释放不执行,当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多线程之间跳动而又得不到执行
    • 对于相互谦让的可以在重试机制中引入随机性,如两台机器尝试使用相同的载波发送数据包,那么这些数据包就会发生冲突,检测到冲突后会在稍后再次尝试发送,如果两者都选择1秒后重试,那么又会发生冲突,并且不断的冲突下去,可以通过设置随机的等待时间来规避这种情况

文章作者: Bryson
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Bryson !
评论
 上一篇
并发-性能与可伸缩性 并发-性能与可伸缩性
影响性能的因素 性能与可伸缩性 可伸缩性指的是: 当增加计算资源时(如CPU丶内存丶存储容量或I/O带宽),程序的吞吐量或处理能力相应地增加 线程引入的开销 在多线程的调度和协调过程中都需要一定的性能开销: 对于为了提升性能而引入的
2019-08-28
下一篇 
并发-线程池的使用 并发-线程池的使用
线程饥饿死锁 在线程池中,如果任务依赖于其他任务,那么可能产生死锁 饥饿: 一个线程在无限的等待另外一个或多个线程相互传递使用并且永不会释放的资源 死锁: 可以认为是两个线程或者进程在请求对方占有的资源 运行时间较长的任务 如果任务阻塞
2019-08-26
  目录