美文网首页
java并发编程 - 3 - 死锁问题

java并发编程 - 3 - 死锁问题

作者: cf6bfeab5260 | 来源:发表于2019-05-10 10:42 被阅读0次

    上一节我们讲到,下面的代码,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接口

    相关文章

      网友评论

          本文标题:java并发编程 - 3 - 死锁问题

          本文链接:https://www.haomeiwen.com/subject/mmhooqtx.html