乐趣区

关于java:线程的活性故障

目录

  • 死锁的产生条件与躲避
  • 死锁的复原
  • 信号失落锁死
  • 嵌套监视器死锁
  • 线程饥饿
  • 活锁

死锁的产生条件与躲避

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

  • 资源互斥:资源必须是独占的,即这个资源只能被一个线程占用
  • 资源不可被争夺:当占用资源的线程不被动开释资源,其它线程无奈获取这个资源
  • 占用并期待资源:当一个线程要获取另一个资源时,如果这个资源被其它线程占用,那么它须要期待其余线程开释这个资源,同时本线程不开释本人占用的资源。
  • 循环期待资源:各个线程都在申请期待其它资源且并不被动开释本身资源,这些线程的申请造成了一个闭环。

    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)是指线程始终无奈取得其所需的资源而导致其工作始终无奈停顿的一种活性故障。

活锁

线程始终在运行状态,然而其所执行的工作却始终没有停顿导致,其始终领有线程却始终不开释并且做一些没有意义的事件。

退出移动版