上一节我们讲到,下面的代码,A账户转帐给B账户A账户锁住了自己等待B账户的锁,同时B账户转账给A账户锁住了自己等待A账户的锁,这个时候就发生了死锁,会永久等待,像极了爱情。。
public class SyncExample {
public int money=200;
public void transfer(SyncExample target,int amt){
synchronized(this){
synchronized (target){
this.money=this.money-amt;
target.money=target.money+amt;
}
}
}
}
发生了死锁怎么解决呢? 目前来说就是重启,但是重启的代价太大了(写故障报告、扣工资)。所以对于死锁我们以预防为主。
1 死锁发生的4大条件。
一位叫 Coffman 的歪果大神整理出了死锁发生的4大条件:
1 互斥: 共享资源同一时刻只能被多个线程中的其中一个占有。
2 占有且等待:线程A占有共享资源V等待共享资源P,在等待的时候,不会主动释放共享资源V。
3 不可抢占:不能强行抢占其他线程占有的共享资源。
4 相互等待:线程A等待B的资源,线程B等待A的资源。
2 如何预防死锁
只有4个都满足的时候,才可能发生死锁,所以我们只需要破坏这4个中的其中一个就可以了。互斥肯定是不能破坏的,不然就不是并发程序了。
2.1 破坏“占有且等待”
再用一个“管理员”去控制,转出账户锁合转入账户锁只能同时获得和释放:
private static List<Object> manager = new ArrayList<>();
private int money;
// 一次性申请所有资源
private static boolean apply(
Object from, Object to) {
synchronized (manager) {
if (manager.contains(from) ||
manager.contains(to)) {
return false;
} else {
manager.add(from);
manager.add(to);
}
}
return true;
}
// 归还资源
private static void free(
Object from, Object to) {
synchronized (manager) {
manager.remove(from);
manager.remove(to);
}
}
// 转账
void transfer(AccountExample1 target, int amt) {
// 一次性申请转出账户和转入账户,直到成功
while (!AccountExample1.apply(this, target)) {
try {
// 锁定转出账户
synchronized (this) {
// 锁定转入账户
synchronized (target) {
if (this.money > amt) {
this.money -= amt;
target.money += amt;
}
}
}
} finally {
AccountExample1.free(this, target);
}
}
}
只有当申请到这两个账户的时候,才会进入转账逻辑,这个时候去对两个账户加锁就一定两个账户的锁都能拿到。这里的代码还有一个可以优化的地儿在于while循环是一个如果一直没等到锁会一直循环,浪费系统资源。 解决方案是改成:当获得的锁释放的时候,通知其他等待锁的线程.
public class AccountExample11 {
private static List<Object> manager = new ArrayList<>();
private int money;
// 一次性申请所有资源
private static boolean apply(
Object from, Object to) {
synchronized (manager) {
if (manager.contains(from) ||
manager.contains(to)) {
try {
manager.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
manager.add(from);
manager.add(to);
}
return true;
}
// 归还资源
private static void free(
Object from, Object to) {
synchronized (manager) {
manager.remove(from);
manager.remove(to);
manager.notifyAll();
}
}
// 转账
void transfer(AccountExample11 target, int amt) {
// 一次性申请转出账户和转入账户,直到成功
while (!AccountExample11.apply(this, target)) {
try {
// 锁定转出账户
synchronized (this) {
// 锁定转入账户
synchronized (target) {
if (this.money > amt) {
this.money -= amt;
target.money += amt;
}
}
}
} finally {
AccountExample11.free(this, target);
}
}
}
}
这里有个小细节,我们用了notifyAll() 没有用 notify()。原因在于,后者会导致可能有一个线程永远都不会被通知到,所以推荐尽量使用notifyAll。
2.2 破坏“不可抢占”
不可抢占这一点,synchronized是做不到的,java.util.concurrent.Lock 是可以做到,这个包我们后面再讲。
2.3 破坏“相互等待”
只需要对资源进行排序,比如A账户是1,B账户是2,C账户是3,申请资源的时候按顺序申请,比如从小到大申请:
public class AccountExample2 {
private int id;
private int money;
// 转账
void transfer(AccountExample2 target, int amt) {
AccountExample2 left = this;
AccountExample2 right = target;
if (this.id > target.id) {
left = target;
right = this;
}
// 锁定序号小的账户
synchronized (left) {
// 锁定序号大的账户
synchronized (right) {
if (this.money > amt) {
this.money -= amt;
target.money += amt;
}
}
}
}
}
由于是从小到大的申请顺序,所以不会发生大的先锁住然后等待小的的情况,只会等待更大的,所以不会发生互相等待的情况。
下一章 Lock和Condition接口
网友评论