liveness hazard—死锁,活锁,饥饿

 在java多线程编程中,一不小心就会引起活性危险(liveness hazard),需要非常谨慎。
liveness可以翻译成活性,如果线程的活性好,说明运行状态不错,能得到CPU的有效处理;反之则线程可能处于死锁、饥饿、活锁等危险状态。
-----------------------------------------------------------------------------------------------------------------------------------------
什么是死锁,举个简单的例子:两只山羊过独木桥,它们同时走到桥中间,结果都堵在中间动弹不得。在这个例子中,可以把一只山羊在桥上走过的路比喻成资源,两只山羊同时在请求对方占有的资源。在java中,这种资源可以理解为对象的监视锁。

以下四个条件若同时满足,则可能会引起死锁:
相互排斥(Mutual exclusion):资源不能被共享,只能由一个线程使用。
请求与保持(Hold and wait):已经得到资源的线程可以再次申请新的资源。例如,线程A和B都需要访问一个文件,同时需要用到打印机,线程A得到了文件资源,线程B得到了打印机资源,但两个进程都不能获得全部的资源。
不可剥夺(No pre-emption):已经分配的资源不能从相应的线程中被强制地剥夺,即使该线程处于阻塞状态。
循环等待(Circular wait):系统中若干进程组成环路,该环路中每个线程都在等待相邻进程正占用的资源。例如,线程A在等待线程B,线程B在等待线程C,而线程C又在等待线程A。

死锁的解决办法有:加锁顺序、加锁时限、死锁检测,具体可参考:
http://ifeve.com/deadlock-prevention/
-----------------------------------------------------------------------------------------------------------------------------------------
什么是活锁,处于活锁状态中的线程对任务的处理丝毫取不到任何进展,其实活锁只是一种形象的比喻,和锁没有太大的关系。举个简单的例子:JMS中的死信问题,即消息监听器监听到消息后进行处理,但由于程序的bug,异常导致事务回滚,消息又回到了消息队列中,消息监听器再次监听到此消息后,又接着处理,又失败回滚,如此循环下去,此消息始终无法得到正确处理。
以下java代码也会引起活锁:
在a线程里:b.join(); 然后在b线程里:a.join()。两个线程都处于等待状态,如下图所示:


liveness hazard—死锁,活锁,饥饿
 

 
活锁的解决办法有:设置最大重试次数,超过该阈值后,监听自动停止;消息监听器监听到消息后,若不成功,则存表,不作回滚处理,事后再进行定时异步补偿处理;编程时避免两个线程互相join的情况,实际很少会出现这种情况。
-----------------------------------------------------------------------------------------------------------------------------------------

什么是饥饿,如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为“饥饿”。

 

例一:

先来了解读写锁的概念,假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写。也就是说:读-读能共存,读-写(写-读)不能共存,写-写不能共存。这就需要一个读/写锁来解决这个问题。

 

再来看Java中的读写锁的实现类—ReentranctReadWriteLock默认使用非公平模式(不是先来先处理的模式)的情况下,如果某个线程想要读取资源,只要没有线程正在对该资源进行写操作且没有线程请求对该资源的写操作即可。如果读操作发生的比较频繁,我们又没有提升写操作的优先级,那么就会产生“饥饿”现象。请求写操作的线程会一直阻塞,直到所有的读线程都从ReentranctReadWriteLock上解锁了。如果一直保证新线程的读操作权限,那么等待写操作的线程就会一直阻塞下去,结果发生“饥饿”。

 

例二:

线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。

以下java代码会引起这种类型的饥饿:

synchronized(obj) {

while (true) {

  // .... infinite loop

}

}

 

例三:

高优先级线程吞噬所有的低优先级线程的CPU时间。例如在java中调用了Thread.setPriority方法设置了线程优先级,优先级低的线程始终得不到执行的机会,虽然线程优先级对于不同操作系统的实现方式不一样,即便设置了优先级也不一定会有效果,但还是有可能会出现这种情况。

 

饥饿的解决办法有:提升写请求的优先级或者采用公平策略;在synchronized方法或者块中避免无限循环;采用线程默认的优先级。
-----------------------------------------------------------------------------------------------------------------------------------------

总结:

死锁是两个线程同时在请求对方占有的资源

 

活锁是线程对任务的处理没有取得任何进展;

 

饥饿是一个线程在无限等待其他线程占有的但是不会往外释放的资源

 

参考资料:

Java Concurrency in PracticePartIII -> Chapter10 -> Avoiding Liveness Hazards

 

饥饿和公平

http://ifeve.com/starvation-and-fairness/

你可能感兴趣的:(多线程)