JAVA并发编程(七)-线程活性故障

Scroll Down

JAVA并发编程(七)-线程活性故障

死锁

死锁是线程的一种常见活性故障,如果两个或者更多的线程因相互等待对方而被永久暂停(生命周期状态为:blockd或者waiting),那么我们称这些线程产生了死锁.

死锁的典型场景是:线程L1持有锁A,去申请锁B,而线程L2在持有锁B的情况下去申请锁A,并且L1和L2都只有在持有到对方的锁之后才会释放自己持有的锁,导致两个线程都无法获得所申请的锁,两个线程都处于无限等待的状态,即产生了死锁.

死锁产生的条件

产生死锁的情况必须要满足下列必要条件:

  • 资源互斥:涉及的资源必须是独占的且每次只允许一个线程持有使用
  • 资源不可抢夺:涉及的资源只能够被持有者主动释放,无法被动释放.
  • 占用并等待资源:涉及的线程当前至少持有一个资源(资源A)并申请另一个资源(资源B)
  • 循环等待资源:涉及的线程必须在等待别的线程持有的资源,而这些线程又反过来等待第一个线程持有的资源

死锁典型的代码特征:

public void deadLock(){
    synchronized(lockA){
        //do
        synchronized(lockB){
        //do    
        }
    }
}
public void deadLock(){
    lockA.lock();
    try{
        //do
        lockB.lock();
        try{
            //do
        }finally{
            lockB.unlock();
        }
    }finally{
        lockA.unlock();
    }
}

规避死锁

粗锁法

使用粗粒度的锁代替多个锁,将原有分开多个锁的资源合并成一个大锁,这样涉及的线程只能够申请一个锁从而避免了死锁

public void deadLock(){
    synchronized(globalLock){
        //doA
        //doB
    }
}

粗锁法的缺点是明显的降低了并发性并导致资源浪费

锁排序法

相关线程使用全局统一的顺序申请锁,假设有多个线程申请资源,那么需要让这些线程按照一个全局统一的顺序去申请这些资源,可以消除循环等待资源的条件,从而规避死锁

锁申请超时

使用ReentrantLock.tryLock(long,TimeUnit)申请锁,ReentrantLock.tryLock(long,TimeUnit)允许我们设置一个超时时间,如果超过这个超时时间还未成功申请到锁则会返回false,可以用于避免占用并等待资源条件

开放调用

ClassA有两个同步方法syncA,syncB,ClassB有两个同步方法syncC,syncD,而其中syncA会去调用syncC,syncD会去掉调用syncB,这样一个线程在执行ClassA.syncA()时,另一个线程在执行ClassB.syncD()就会导致产生死锁.

开放调用就是在调用外部方法时不持有任何锁,可以使用一些并发集合类处理一些共享变量,使调用外部方法前都不持有锁,因此也不会导致两者之间互相等待从而规避了死锁.

锁死

等待线程由于唤醒其所需的条件永远无法成立,或者其他线程无法唤醒这儿线程而一直处于非运行状态(线程并未终止)导致任务一直无法得到进展,称这个线程为锁死(LockOut)

信号丢失锁死

信号是锁死是由于没有相应的通知线程来唤醒等待线程从而使得线程一直处于等待状态的一种活性故障.

常见是由于countDownLatch.countDown()并未放在finally代码块中导致countDownLatch.await()的执行线程一直处于等待状态,从而使任务一直无法得到进展

嵌套监视器锁死

由于嵌套锁导致等待线程永远无法被唤醒从而造成锁死.

嵌套监视器代码特征:

public void waitThread(){
    synchronized(A){
        synchronized(B){
            while(flagTrue){
                B.wait()    
            }
            //doOther
        }
    }
}
public void notifyThread(){
    synchronized(A){
        synchronized(B){
            flagTrue = false;
            B.notifyAll();
            //doOther
        }
    }
}

等待线程wait的时候会释放B锁,但是不会释放A锁,而通知线程又需要持有A锁与B锁才能通知B这个等待线程,导致等待线程一直无法被唤醒,这种称为嵌套监视器锁死.

线程饥饿

线程饥饿是由于线程一直无法获得其所需的资源而导致任务一直无法进展的一种活性故障,在高争用的非公平调度模式下可能会产生

典型的场景是:一个业务配置的读取与更新使用读写锁来读取配置数据.默认情况下ReentrantReadWriteLock是非公平模式,这可能就会导致在业务线程频繁的读取配置数据时并占据读锁,导致更新线程无法获得对应写锁从而无法更新配置数据.

活锁

活锁是指一直线程一直处于运行状态,但是其任务却一直无法进展的一种活性故障,其实线程饥饿如果一直在申请所需资源而未申请成功,则演变成活锁.