接上,死锁问题
1.原因
定义之前已经阐述,这里先上一个死锁最简单的例子:
//线程1:
public void leftRight() {
// 得到left锁
synchronized (left) {
// 得到right锁
synchronized (right) {
doSomething();
}
}
}
//线程2:
public void rightLeft() {
// 得到right锁
synchronized (right) {
// 得到left锁
synchronized (left) {
doSomethingElse();
}
}
}
可以看到,1线程锁住left,现在要right,不给就等,等的时候left也不解锁;2线程同时开始运行,于是锁住right,要left,不给也等,等的时候right也不解锁。于是谁都进行不下去。
结合例子看死锁条件:
1.互斥:即独占,synchronized锁是表面原因,一个人占用了就不给其他人用,否则1线程就不需要等待left了,直接拿来用就行了。根本原因是被锁的东西的属性,属性要求不能被同时共享,如打印机,如对账户的修改。
2.不剥夺:即线程1占用了left之后,不会因为你2来了就会给你,只有我运行完了才释放,2也是这么想的。
3.保持和请求:如果单单不剥夺,比如线程1只能占用一个线程,现在占着left,就够用了,不会要求right也就没事了,但还是必须要吃着碗里的看着锅里的才会导致死锁。
4.循环等待:对资源的需求链要能构成闭环,死锁不是卡住,错误原因必须要是来自逻辑的谬误而非打印机坏了:2欠了1的钱,3欠了2的钱,3欠了1的钱,大家在内部越借越多,全是次级信贷,借了一圈到了该还钱的都傻眼了,金融危机爆发;如果123都是找我要钱,我就是不给,虽然后果是一样的,但不能称之为死锁。
2.如何避免
其实标题应该是编程中如何避免死锁,有哪些避免的措施,像银行家算法这类操作系统层面的东西暂不考虑
1.保证上锁顺序
举个例子,设线程1,2用到的资源并集是ACD(即两个都用的资源),当线程1依次对ACD上锁时,我们发现,如果2对资源中A,C,D,无论何时,无论和其他资源什么顺序,只要保证这三个资源的上锁顺序是依次的,就可以避免死锁。其实相当于ACD三个资源合并了。
举一个常用的转账例子:
从账户from转到账户to,显然这两个账户都要上锁,如果这么写:
//先锁转出账户
synchronized (fromAccount) {
//再锁转入账户
synchronized (toAccount) {
//查询是否有余额
if (fromAccount.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
else {
//转出账户减钱,转入的加钱
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
}
看起来是很有道理的,所有人都先锁转出账户,再锁转入账户,完美。
其实这帮助我们认识到一个常见的顺序上锁误区:当我们在谈论A,C,D时,我们在谈论什么?
这时刚刚那个转账的方法头:
public static void transferMoney(Account fromAccount,Account toAccount,DollarAmount amount)
现在有这样一个场景:我在淘宝上买东西,给马云10块,马云分红,给我1亿:
//taobao买东西
public static void transferMoney(Account myAccount,Account mayunAccount,DollarAmount 10)
//分红
public static void transferMoney(Account mayunAccount,Account myAccount,DollarAmount 100000000)
线程1会先锁我的账户并要马云账户锁,线程2会先锁马云账户并要我账户锁,仍然死锁了。所以from和to只是形参而已,不是资源的肉身。引入两个方法的概念:
S1.hashCode:根据S1的字符序列计算其哈希值;
S1.identityHashCode:根据S1的内存地址来计算哈希值。
内存地址可以在一些情况下确定资源的肉身:
public class InduceLockOrder {
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 (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
}
}
}}
总结来说,当有100个资源要共享时,要为他们每个人安排在这个独一无二的标号,不管每个线程申请哪些,都按照从小到大(一个顺序,都是大到小也可以)的顺序加锁。
2.时限锁
打破了保持和请求的条件,拿不到不头铁,不硬等。例如使用可中断锁trylock():没获取返回false,
boolean tryLock(long time, TimeUnit unit):在设置等待时间内没有获取则返回false。
3.检测
可以用工具对代码进行审查,参考资料:https://blog.csdn.net/abc86319253/article/details/49534225
原理大致是在一个线程获取锁时,会有记录在一个数据结构中,通过遍历这个结构形成关系图,查看对资源的占有是否满足死锁。
我不知道多少人会由此想到图灵的停机问题:
图灵在1936年证明这样一个算法是不存在的:该过程以一个计算机程序以及该程序的一个输入作为输入,并判断该过程在给定输入运行时是否最终能停止。
??? 检测工具不就是能判断输入程序是否会终止咩?
这段博客中的话着实不严谨,真正的问题描述不是“一个程序作为输入”而是“任意一个程序作为输入”,它的证明方法也是将自身作为程序带入自身。(话说,把代码审查的代码带入代码审查会发生什么?----之后开一个坑,分享看《论可计算数》学到的知识)