头图

线程的活性故障

eacape
English

目录

死锁的产生条件与规避

产生一个死锁必须满足以下所有条件

  • 资源互斥:资源必须是独占的,即这个资源只能被一个线程占用
  • 资源不可被抢夺:当占用资源的线程不主动释放资源,其它线程无法获取这个资源
  • 占用并等待资源:当一个线程要获取另一个资源时,如果这个资源被其它线程占用,那么它需要等待其他线程释放这个资源,同时本线程不释放自己占用的资源。
  • 循环等待资源:各个线程都在请求等待其它资源且并不主动释放自身资源,这些线程的请求形成了一个闭环。

    T1占用资源A等待资源B被T2释放同时自己不释放资源A

    T2占用资源B等待资源C被T3释放同时自己不释放资源B

    T3占用资源C等待资源D被T4释放同时自己不释放资源C

    T4占用资源D等待资源A被T1释放同时自己不释放资源D

想要避免死锁只要消除以上任何一个条件即可

规避死锁的方式

  • 锁粗化:例如哲学家问题中,筷子是只能被独占的,每个哲学家同时去拿筷子,然后哲学家们同时都拿起了自己左手的筷子,但是自己右边的筷子却被隔壁的哲学家握在左手,然后它们都在等待隔壁的哲学家放下左手的筷子,结果谁也放就形成了死锁。

    上面相当于哲学家A、哲学家B、哲学家C获取筷子A、筷子B、筷子C,相当于线程A、线程B、线程C获取到了Lock-A、Lock-B、Lock-C!

    我将这个锁"粗化"为,我们可以让每次此只能由一个哲学家取筷子,这样至少有一个哲学家拿到了左右两根筷子,以至于不会形成循环等待资源的局面。

    实际上就是用Lock来代替Lock-A、Lock-B、Lock-C这样同时只会有一个线程获取到资源,这样就可以有效避免死锁的发生,但是缺点也很明显就是降低了并发的可能性,导致资源浪费!

  • 锁排序

    依然是哲学家问题,我们给筷子加个序号 筷子A(1) 、筷子B(2)、筷子C(3),我们在拿筷子的时候要去判断左手旁的筷子序号小还是右手的序号小,哪个小我们先拿哪根筷子。

    可能会出现以下场景

    哲学家A先拿起筷子A(1)然后拿起了筷子C(3),然后哲学家B发现自己右手的筷子A序号更小,但是已经被哲学家A拿到了,所以他不会拿筷子B(2),然后哲学家C发现自己右手的筷子B(2)序号小于自己左手的筷子C(3),但是需要等待哲学家A放下筷子C(3)才能吃饭。

    上面哲学家的拿筷子的排列会有很多,但是由于需要按照左右手筷子的序号大小来拿起筷子(不会每个哲学家左手筷子的序号大于右手,也不会每个哲学家右手筷子的序号大于左手)所以不会出现循环等待的情况。

  • 使用tryLock(long, TimeUnit)来申请锁,当一个线程申请一个资源的时间达到一定的界限后会放弃申请这把锁,这样就不会导致循环等待资源的场面了。

    例如哲学家问题中假如每个哲学家都拿到了右手的筷子,左手都在等待隔壁放下筷子,但是一定的时间内等不到筷子就放弃等待并且把自己的右手的筷子也放下。

    try{
      leftLock.tryLock()
    }catch(interruptedException e){
      rightLock.unLock();
      ......  
    }
    ....

死锁的恢复

在java中使用内部锁或者Lock.lock()方式申请的锁并且产生了的死锁是不可恢复的,只能通过重启虚拟机来去除这些死锁。但是如果线程是通过Lock.lockInterruptibly()方式申请的锁,那么死锁是可能被调用thread.interrupt()打破的。

public class DeadlockDetector extends Thread {
  static final ThreadMXBean tmb = ManagementFactory.getThreadMXBean();
  /**
   * 检测周期(单位为毫秒)
   */
  private final long monitorInterval;

  public DeadlockDetector(long monitorInterval) {
    super("DeadLockDetector");
    setDaemon(true);
    this.monitorInterval = monitorInterval;
  }

  public DeadlockDetector() {
    this(2000);
  }

  public static ThreadInfo[] findDeadlockedThreads() {
    long[] ids = tmb.findDeadlockedThreads();
    return null == tmb.findDeadlockedThreads() ?
        new ThreadInfo[0] : tmb.getThreadInfo(ids);
  }

  public static Thread findThreadById(long threadId) {
    for (Thread thread : Thread.getAllStackTraces().keySet()) {
      if (thread.getId() == threadId) {
        return thread;
      }
    }
    return null;
  }

  public static boolean interruptThread(long threadID) {
    Thread thread = findThreadById(threadID);
    if (null != thread) {
      thread.interrupt();
      return true;
    }
    return false;
  }

  @Override
  public void run() {
    ThreadInfo[] threadInfoList;
    ThreadInfo ti;
    int i = 0;
    try {
      for (;;) {
        // 检测系统中是否存在死锁
        threadInfoList = DeadlockDetector.findDeadlockedThreads();
        if (threadInfoList.length > 0) {
          // 选取一个任意的死锁线程
          ti = threadInfoList[i++ % threadInfoList.length];
          Debug.error("Deadlock detected,trying to recover"
                      + " by interrupting%n thread(%d,%s)%n",
                  ti.getThreadId(),
                  ti.getThreadName());
          // 给选中的死锁线程发送中断
          DeadlockDetector.interruptThread(ti.getThreadId());
          continue;
        } else {
          Debug.info("No deadlock found!");
          i = 0;
        }
        Thread.sleep(monitorInterval);
      }// for循环结束
    } catch (InterruptedException e) {
      // 什么也不做
      ;
    }
  }
}

通过调用Management.ThreadMXBean.findDeadlockedThreads()方法可以检测到虚拟机中存在死锁的线程,然后对这些线程的死锁进行打断,来实现死锁的恢复。

因为死锁产生的原因是不可控的,因此死锁恢复的可操作性并不强,甚至可能在死锁的打断过程中产生活锁等新的问题。

信号丢失锁死

信号丢失锁死的一个典型例子是等待线程在执行Object.wait()/Condition.await()前没有对保护条件进行判断,而此时保护条件实际上可能已然成立,然而此后可能并无其他线程更新相应保护条件涉及的共享变量使其成立并通知等待线程,这就使得等待线程一直处于等待状态,从而使其任务一直无法进展。

嵌套监视器死锁

嵌套锁可能导致线程始终无法通知唤醒等待线程的活性故障就被称为嵌套监视器死锁。

public class NestedMonitorDeadlockDemo {
    static Object lockA = new Object();
    static Object lockB = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                synchronized (lockA){
                    synchronized (lockB){
                        try {
                            lockB.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };

        Thread t2 = new Thread(){
            @Override
            public void run() {
                synchronized (lockA){
                    synchronized (lockB){
                        lockB.notifyAll();
                    }
                }
            }
        };
    }
}

如上代码t1线程中lockB.wait()意味着t1线程释放了lockB,但是lockA并不会被其释放,这样的话t2就永远无法获取到lockA从而就不会执行lockB.notifyAll()这样的话t1就会永远不会被唤醒,t2线程就一直会卡在lockA的获取中。

嵌套监视器死锁一般不会像上面代码中这么"明明晃晃"的出现,而是一般会在使用一些api的时候出现,使用阻塞队列模拟一个消息队列,如下图

基于对上图中getMsg方法和setMsg方法的源码深层拆解可将这两个锁的使用情况总结如下

==getMsg==
sychronized(NesredMonitorDeadLocalDemo.class){
  lock.lockInterruptibly();
  while(条件){
    notEmpty.await();
  }
  notFull.signal();
  lock.unlock();
}

==setMsg==
sychronized(NesredMonitorDeadLocalDemo.class){
  lock.lockInterruptibly();
  while(条件){
    notFull.await();
  }
  notEmpty.signal();
  lock.unlock();
}

经过拆解后发现,这就是一个经典的嵌套监视器死锁,当一个线程执行getMsg阻塞时会释放显式锁但是并不会释放最外层的内部锁的,另外的线程去访问setMsg的时候就永远不会获取这个内部锁。

线程饥饿

线程饥饿(Thread Starvation)是指线程一直无法获得其所需的资源而导致其任务一直无法进展的一种活性故障。

活锁

线程一直在运行状态,但是其所执行的任务却一直没有进展导致,其一直拥有线程却一直不释放并且做一些没有意义的事情。

阅读 442

JAVA 攻城狮

170 声望
4 粉丝
0 条评论

JAVA 攻城狮

170 声望
4 粉丝
文章目录
宣传栏