您的位置:首页 > 编程语言

并发编程实战学习笔记(七)——避免活跃性问题

2017-03-26 11:05 495 查看

锁顺序死锁

定义

试图以不同的顺序去获得相同的锁,就可能会产生死锁

解决办法

如果所有线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题

动态的锁顺序死锁

原因

锁顺序本身是动态的,无法通过相同的顺序来避免死锁问题

解决办法

通过一致哈希算法或者其它方式来统一锁顺序,使未知顺序变为已知顺序。对于极少数的哈希冲突,可以使用“加时赛”锁来解决

private static final Object tieLock = new Object();

public void transferMoney(final Account fromAcct,
final Account toAcct,
final DollarAmount amount)
throws InsufficientFundsException{
class Helper{
public void transfer throws InsufficientFundsException{
if(fromAcct.getBalance().compareTo(amount) < 0){
throw new InsufficientFundsException();
}else{
fromAcct.debit(amount);
toAcct.credit(amount);
}
}
}

int fromHash = System.identifyHashCode(fromAcct);
int toHash = System.identityHashCode(toAcct);

if(fromHash < toHash){
synchronized(fromAcct){
synchronized(toAcct){
new Helper.transfer();
}
}
}else if (fromHash > toHash) {
synchronized(toAcct){
synchronized(fromAcct){
new Helper().transfer();
}
}
} else {
synchronized(tieLock){//加时赛锁来解决问题
synchronized(fromAcct){
synchronized(toAcct){
new Helper().transfer();
}
}
}
}
}


在协作对象之间发生的死锁

如果在持有锁时调用某个外部方法,那么将出现活跃性问题。在这个外部方法中可能会获取其它锁(这可能会产生死锁),或者阻塞时间过长,导致其它线程无法及时获得当前被持有的锁。

锁顺序死锁可能以这种方式隐式出现

开放调用

如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用

简化并发程序分析难度的可行思路

虽然在没有封装的情况下也能确保线程安全的程序,但对一个使用了封装的程序进行线程安全分析,要比分析没有使用封装的程序容易得多。

分析一个完全依赖开放调用的程序的活跃性,要比分析那些不依赖开放调用的程序的活跃性简单。

开放调用的改造思路

锁消除;去掉本来要持有的锁

细粒度锁;只锁一小块,调用其它方法时就不需要锁了。

资源死锁的类型

独占类型的访问都可以和加锁操作类比,看起来就像需要获得锁才能访问。

如果一个任务需要连接两个数据库,并且在请求这两个资源时不会始终遵循相同的顺序,那么线程A可能持有与数据库D1的连接,并等待与数据库D2的连接,而线程B持有D2的连接并等待与D1的连接。资源池越大,就越不容易出现这种类型的死锁。

线程饥饿死锁。如果某些任务需要等待其它任务的结果,那么这些任务往往是产生线程饥饿死锁的主要来源,有界线程池/资源池与相互依赖的任务不能一起使用。

死锁的避免思路

尽量依赖开放调用。如果一个程序每次至多只能获得一个锁,那么就不会产生锁顺序死锁。

如果必须获取多个锁,那么在设计时必须考虑锁的顺序:尽量减少潜在的加锁交互数量,将获取锁时需要遵循的协议写入正式文档并始终遵循这些协议。

显示使用Lock类中的定时tryLock功能来代替内置锁机制。

定时锁的优点

当定时锁失败时,你并不需要知道失败的原因。至少你能记录所发生的失败,以及关于这次操作的其它有用信息,并通过一种更平缓的方式来重新启动计算,而不是关闭整个进程。

如果在获取锁时超时,那么可以释放这个锁,然后后退并在一段时间后并再次尝试,从而消除了死锁发生的条件,使程序恢复过来。(这项技术只有在同时获取两个锁时才有效,如果在嵌套的方法调用中请求多个锁,那么即使你知道已经持有了外层的锁,也无法释放它。)

饥饿

当线程由于无法访问它所需要的资源而不能继续执行时,就发生了“饥饿”,引发饥饿的最常见资源就是CPU时钟周期。

问题

我们尽量不要改变线程的优先级。只要改变了线程的优先级,程序的行为就将与平台相关

解释

在Thread API中定义了10个优先级,JVM根据需要将它们映射到操作系统的调度优先级。这种映射与特定平台相关的,因此在某个操作系统中两个不同的Java优先级可能被映射到同一个优于级,而在另一个操作系统中则可能被映射到另一个不同的优先级。在某些操作系统中,如果优先级的数量少于10个,那么有多个java优先级会被映射到同一个优先级。

活锁

活锁是另一种形式的活跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。

过度错误恢复代码

程序捕捉到错误消息时,会重新把错误消息放入队列或者直接重复操作,这种过度的错误恢复机制,直接导致程序一直在“处理-错误-发现-重新处理”的循环中无法跳出

解决办法就是识别出这种消息,并且跳过处理

不恰当地礼让

当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。这就像两个过于礼貌的人在半路上面对面地相遇,他们彼此都让出对方的路,然而又在另一条路上相遇。因此他们就这样反复地避让下去。

要解决这种活锁问题,需要在重试机制中引入随机性。在并发应用程序中,通过等待随机长度的时间和回退可以有效避免活锁的发生。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  并发 编程 线程