java并发编程中的活跃性问题

合理的使用多线程,可以提高程序的响应能力,吞吐能力;能够提高硬件资源的利用率。但是如果对多线程不加以合理的利用:比如说对资源不合理的加锁。可能会造成很多的活跃性问题。那么会造成哪些活跃性问题呢?

 一.死锁

       死锁的经典问题:哲学家问题。大家应该已经耳熟能详了。五个哲学家围绕着同一张桌子吃饭,桌子上共有5根筷子,放在他们每两个人的中间。哲学家们呢,时而思考,时而拿起左右两边的筷子就餐,吃完呢,就把筷子再放回原来的位置。如果每个人都立即拿起自己左边的筷子不放,那么这五个哲学家,就饿死了。这个问题产生的原因就是每个人都拥有其他人需要的资源,并且在等待其他人释放资源,而且每个人再获得到自己需要的资源之前都不会放弃已经拥有的资源。

        怎么解决上边的问题呢?其实如果某个哲学家尝试获得两根筷子失败的时候,其中一根筷子被其他人使用的话,他放弃自己已经获得的筷子,过段时间再去尝试。就会解决上面的问题。

       

        线程A持有锁L并且同时想获取锁M,与此同时线程B持有锁M的同时想获取锁L。两个线程互不相让,最终导致死锁。这是最简单的死锁形式。其实如果多个线程存在环路的锁依赖关系都将产生死锁。java中是没有像数据库那样的死锁检测的机制的,如果发生了死锁,没有外部干预的话,发生死锁的线程都将会等待在那里。

       

       下面我们站在代码的角度看一下死锁产生的情况,通过了解这些内容,我们在工作中,也就会避免一些产生死锁的场景。

1.锁顺序死锁:不同的线程以不同的顺序获得两个相同的锁。

扫描二维码关注公众号,回复: 492238 查看本文章
public class LeftRightLock {
    private final Object left = new Object();
    private final Object right = new Object();

    public void leftRight() {
        synchronized (left) {
            synchronized (right) {
                // doSomething
            }
        }
    }

    public void rightLeft() {
        synchronized (right) {
            synchronized (left) {
                // doSomething
            }
        }
    }
}

当一个线程调用leftRight方法的同时另一个线程正在调用rightLeft方法,那么程序就很容易因为这种循环的所依赖发生死锁。

 2.动态的锁顺序依赖:

public class TransferMoney {
    public void transferMoney(Account fromAccount, Account toAccount, Integer amount) {
        synchronized (fromAccount) {
            synchronized (toAccount) {
                // doSomething
            }
        }
    }
}
上诉代码看似每什么问题,都是以相同的顺序来获取锁的,按理说不应该会发生死锁问题。但是我们从方法调用的角度看,如果两个不同的线程同时调用这个方法,一个是从Account A转账到Account B;另一个是从
Account B转账到Account A(这里全局每一个账户对应一个对象),那么照样还是会出现锁顺序依赖导致的死锁问题。

 

 上面这两个问题都是因为锁顺序依赖的问题导致的死锁,那么如果想要避免因为这种情况导致的死锁问题,我们一下便想的到的方法是,我们自己去定义锁的顺序。这样不就避免了因为以不同的顺序调用两个相互关联的锁导致的死锁问题了么?

3.公开协作对象之间的调用引起的死锁:

class Taxi {
    private final Dispatcher dispatcher;
    private Point location, destination;

    public Taxi(Dispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }

    public synchronized Point getLocation() {
        return location;
    }

    public synchronized void setLocation(Point location) {
        this.location = location;
        if (location.equals(destination)) {
            dispatcher.notifyAvailable(this);
        }
    }
}

class Dispatcher {
    private final Set<Taxi> taxis;
    private final Set<Taxi> availableTaxis;

    public Dispatcher() {
        taxis = new HashSet<Taxi>();
        availableTaxis = new HashSet<Taxi>();
    }

    public synchronized void notifyAvailable(Taxi taxi) {
        availableTaxis.add(taxi);
    }

    public synchronized Image getImage() {
        Image image = new Image();
        for (Taxi t : taxis) {
            image.drawMarker(t.getLocation());
        }

        return image;
    }

}

Taxi代表一个出租车对象,Dispatcher代表一个调度系统。从单个对象上来看,没有显示的锁依赖。但是我们从两个对象协作的角度来看,getImage和setLocation两个方法都涉及到同时去获得两个锁。如果一个线程持
有Dispathcer锁等待Taxi锁;另一个线程持有Taxi锁等待Dispatcher锁。那么就会造成死锁。

 4.由于对与有限资源的竞争造成的死锁:

       比如有两个资源池,池子里边存放的是不同的两种资源,并且每个池子里边只有一个资源;某两个线程同时需要得到这两种资源,这两个线程如果各自获得了其中一个资源,与此同时在等待另一个池子里边有空闲资源。那么就会造成死锁。这种情况如果资源越少越容易发生死锁。

如何解决避免死锁问题?

     针对死锁问题,我们可以采用以下几种方法来避免死锁的产生:

  • 限制锁的调用顺序。
  • 缩小锁的范围。
  • 使用显示锁替换内置锁。显示锁可以有更加灵活的锁的策略,比如可以指定一定时间范围获得不了锁的话,可以进行失败,进行锁失败策略处理。而不是一味的等待。

二.饥饿

      饥饿也是多线程应用中的一种常见的活跃性问题。由于线程得不到需要的资源,不能正常执行,就会造成线程饥饿。比如说,对线程的优先级设置不当,造成线程不能获得CPU周期执行导致饥饿,或者说其他线程长时间持有锁,导致其他线程长时间等待,造成的饥饿。

三.活锁:

      活锁问题不会导致线程阻塞,但是活锁会导致线程不能继续正常执行。比如这样一个消息系统中,从队列里边取的消息,然后执行,但是由于某种业务原因,失败了,那么把它放到队列头,然后再拿出来执行,自然还是失败的,这样线程虽然没有阻塞,但是也不能正常的处理其他的消息。

       要解决上诉问题,还需要涉及合理的重试策略。

   

    

   

    

猜你喜欢

转载自study-a-j8.iteye.com/blog/2366489