安全性和活跃度通常相互制约。我们使用锁来保证线程安全,但是滥用锁可能引起锁顺序死锁。
一.死锁
当一个线程永远占有一个锁,而其他线程尝试去获得这个锁,那么他们将永远阻塞。当线程A占有锁L时,想要获得锁M,但是同时线程B持有M,尝试获得L,俩个线程将永远等待下去这被称作死锁(或称致命拥抱)。
1.锁顺序死锁
简单的锁顺序死锁
public class LeftRightDeadLock{
private final Object left = new Object();
private final Object right = new Object();
public void leftRight(){
synchronized(left){
doSomething();
}
}
public void leftLeft(){
synchronized(right){
doSomethingElse();
}
}
}
2.动态的顺序死锁
下面的代码中看似没有死锁,但有些巧合下可能发生死锁。
动态加锁顺序产生死锁
public void transferMoney(Account fromAccomunt,Account toAccount,DollarAmount amount)throws InsufficientFundsException{
synchronized(fromAccount){
synchronized(toAccount){
if(fromAccount.getBalance().compareTo(amount)<0)
throws new InsufficientFundsException();
else{
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
}
}
如果俩个线程同时调用transferMoney一个从X向Y转账,另一个从Y向X转账那么就会发生死锁。
为了解决这个问题我们必须制定锁的顺序。并应用在整个程序中,
制定锁的顺序来避免死锁
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.identityHashCode(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 (toAcct){
synchronized (fromAcct){
new Helper.transfer();
}
}
}
}
}
3.协作对象间的死锁
在持有锁的时候调用外部方法是在挑战活跃度问题。外部方法可能会获得其他锁(产生死锁的危险),或者遭遇严重超时的阻塞。当你持有锁的时候会延迟其他试图获得该锁的线程。
4.开放调用
在持有锁的时候调用另一个外部方法很难进行分析,因此是危险的。当调用的方法不需要持有锁是,这被称为开放调用。
在程序中尽量使用开放调用。依赖于开放调用的程序,相比于那些在持有锁的时候还调用外部方法程序,更容易进行死锁自由度的分析。
5.资源死锁
当线程见互相等待对方持有的锁并且谁都不会释放自己的锁时就会发生死锁,当将线程和等待的目标变成资源时,会发生与之类似的死锁。
另一种基于资源的死锁是线程饥饿死锁。
二.避免和诊断死锁
如果一个程序一次至多获得一个锁,那么就不会发生死锁。当然这并不现实。但如果你必须获得多个锁,那么锁的顺序必须是你设计的一部分:尽量减少潜在锁之间的交互数量,遵守文档化该锁顺序协议。
1.尝试定时的锁
显示的锁你可以定义超时时间,在规定时间过后tryLock还没有获得锁就返回失败。
2.通过线程转储分析死锁
预防死锁是面临最大的问题,JVM采用线程转储帮助你识别死锁的发生。线程转储也包括锁的信息,比如锁由哪个线程获得,获得其中的栈结构,以及阻塞线程正在等待的锁究竟是哪一个。在生成转储之前JVM在表示“正在等待”关系的有向图中搜索循环来寻找死锁。如果发现了死锁,它会包括死锁的识别信息,其中参与了哪些锁和线程,以及程序中造成不良后果的锁请求发生在哪里。
三.其他的活跃度危险
死锁是主要的活跃度危险,但是也肯能有其他活跃度危险包括:饥饿。丢失信号,和活锁。
1.饥饿
当线程访问它所需要的资源时却被永久拒绝,以至于不能再继续,这样就发生了饥饿。
2.弱响应性
不良的锁管理也可能引起弱响应性。如果一个线程长时间占用一个锁,其他想要访问该容器的线程就必须等待很长时间。
3.活锁
活锁是线程中活跃度失败的另一种形式,尽管没有阻塞,线程任然不能继续,应为它不断重试相同的操作,却总是失败。