美文网首页
Java 死锁分类

Java 死锁分类

作者: 南风nanfeng | 来源:发表于2019-03-06 20:26 被阅读0次

    1. 死锁简介

    经典的“哲学家进餐”问题很好的描述了死锁的情况。5个哲学家吃中餐,坐在一张圆桌上,有5根筷子,每个人吃饭必须用两根筷子。哲学家时而思考时而进餐。分配策略有可能导致哲学家永远无法进餐。

    类似的,当线程A尝试持有锁L1,并尝试获取锁L2;同时,线程B持有锁L2,并尝试获取锁L1,并且都不释放已经拥有的锁。这就是最简单的死锁。其中存在环状的锁依赖关系。称为“抱死”。

    数据库系统有监视、检测死锁的环节。当两个事务需要的锁相互依赖时,DB将选择一个牺牲者放弃这个事务,牺牲者会释放持有的资源,从而使其他事务顺利的执行。

    JVM在解决死锁问题时并没有数据库系统那么强大,当一组线程发生死锁时,那么这写线程就凉凉——永远不会被使用。

    2. 锁顺序死锁

    经典案例是LeftRightDeadlock,两个方法,分别是leftRigth、rightLeft。如果一个线程调用leftRight,另一个线程调用rightLeft,且两个线程是交替执行的,就会发生死锁。

    public class LeftRightDeadLock {
    
        private static Object left = new Object();
        private static Object right = new Object();
    
        public static void leftRigth() {
            synchronized (left) {
                System.out.println("leftRigth: left lock");
                synchronized (right) {
                    System.out.println("leftRigth: right lock");
                }
            }
        }
    
        public static void rightLeft() {
            synchronized (right) {
                System.out.println("rightLeft: right lock");
                synchronized (left) {
                    System.out.println("rightLeft: left lock");
                }
            }
        }
    
        public static void main(String[] args) {
            new Thread(() -> {
                for (int i=0; i<100; i++) {
                    leftRigth();
                }
            }).start();
            new Thread(() -> {
                for (int i=0; i<100; i++) {
                    rightLeft();
                }
            }).start();
        }
    
    }
    

    3. 动态的锁顺序死锁

    上例告诉我们,交替的获取锁会导致死锁,且锁是固定的。有时候并锁的执行顺序并不那么清晰,参数导致不同的执行顺序。经典案例是银行账户转账,from账户向to账户转账,在转账之前先获取两个账户的锁,然后开始转账,如果这是to账户向from账户转账,角色互换,也会导致锁顺序死锁。

    @Slf4j
    public class TransferMoneyDeadlock {
    
        public static void transfer(Account from, Account to, int amount) throws Exception {
            synchronized (from) {
                log.info("线程【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), from.name);
                synchronized (to) {
                    log.info("线程【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), to.name);
                    if (from.balance < amount) {
                        throw new Exception("余额不足");
                    } else {
                        from.debit(amount);
                        to.credit(amount);
                        log.info("线程【{}】从【{}】账户转账到【{}】账户【{}】元钱成功", Thread.currentThread().getName(), from.name, to.name, amount);
                    }
                }
            }
        }
    
        private static class Account {
            String name;
            int balance;
    
            public Account(String name, int balance) {
                this.name = name;
                this.balance = balance;
            }
            void debit(int amount) {
                this.balance = balance - amount;
            }
            void credit(int amount) {
                this.balance = balance + amount;
            }
        }
    
        public static void main(String[] args) {
            Account A = new Account("A", 100);
            Account B = new Account("B", 200);
            Thread t1 = new Thread(() -> {
                for (int i=0; i<100; i++) {
                    try {
                        transfer(A, B, 1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            t1.setName("T1");
    
            Thread t2 = new Thread(() -> {
                for (int i=0; i<100; i++) {
                    try {
                        transfer(B, A, 1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            t2.setName("T2");
    
            t1.start();
            t2.start();
        }
    }
    

    单单看transfer方法看不出来有leftRight锁顺序死锁,多线程调用该方法后,出现两个账户同时转账的情况,同样引起了锁顺序死锁。下面通过确定两个账户的顺序解决该问题。

    @Slf4j
    public class TransferMoneyOrdered {
    
        private static Object commObj = new Object();
    
        public static void transfer(Account from, Account to, int amount) throws Exception {
            int fromHash = System.identityHashCode(from);
            int toHash = System.identityHashCode(to);
            if (fromHash > toHash) {
                synchronized (from) {
                    log.info("线程: 【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), from.name);
                    synchronized (to) {
                        log.info("线程: 【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), to.name);
                        new Helper().transfer(from, to, amount);
                    }
                }
            } else if (fromHash < toHash) {
                synchronized (to) {
                    log.info("线程【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), to.name);
                    synchronized (from) {
                        log.info("线程【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), from.name);
                        new Helper().transfer(from, to, amount);
                    }
                }
            } else {
                synchronized (commObj) {
                    synchronized (from) {
                        log.info("线程 【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), from.name);
                        synchronized (to) {
                            log.info("线程【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), to.name);
                            new Helper().transfer(from, to, amount);
                        }
                    }
                }
            }
        }
    
        private static class Helper {
            public void transfer(Account from, Account to, int amount) throws Exception {
                if (from.balance < amount) {
                    throw new Exception("余额不足");
                } else {
                    from.debit(amount);
                    to.credit(amount);
                    log.info("线程:【{}】从【{}】账户转账到【{}】账户【{}】元钱成功", Thread.currentThread().getName(), from.name, to.name, amount);
                }
            }
        }
        private static class Account {
            String name;
            int balance;
    
            public Account(String name, int balance) {
                this.name = name;
                this.balance = balance;
            }
            void debit(int amount) {
                this.balance = balance - amount;
            }
            void credit(int amount) {
                this.balance = balance + amount;
            }
        }
    
        public static void main(String[] args) {
            Account A = new Account("A", 100);
            Account B = new Account("B", 200);
            Thread t1 = new Thread(() -> {
                for (int i=0; i<100; i++) {
                    try {
                        transfer(A, B, 1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            t1.setName("T1");
    
            Thread t2 = new Thread(() -> {
                for (int i=0; i<100; i++) {
                    try {
                        transfer(B, A, 1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            t2.setName("T2");
    
            t1.start();
            t2.start();
        }
    }
    
    

    如上,通过账户的hash取值确保锁顺序的一致性,从而避免两个线程交替获取两个账户的锁。

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

    上述两例中,在同一个方法中获取两个锁。实际上,锁并不一定在同一方法中被获取。经典案例,如出租车调度系统。

    @Slf4j
    public class CooperateCallDeadlock {
    
        private static class Taxi {
            private String location;
            private String destination;
            private Dispatcher dispatcher;
    
            public Taxi(Dispatcher dispatcher, String destination) {
                this.dispatcher = dispatcher;
                this.destination = destination;
            }
    
            public synchronized String getLocation() {
                log.info("获取锁Taxi:getLocation");
                return location;
            }
    
            public synchronized void setLocation(String location) {
                log.info("获取锁Taxi:setLocation");
                this.location = location;
                if (location.equals(destination)) {
                    dispatcher.notifyAvailable(this);
                }
            }
        }
    
        private static class Dispatcher {
            private Set<Taxi> availableTaxis;
    
            public Dispatcher() {
                availableTaxis = new HashSet<>();
            }
    
            public synchronized void notifyAvailable(Taxi taxi) {
                log.info("获取锁Dispatcher:notifyAvailable");
                availableTaxis.add(taxi);
            }
    
            public synchronized void driveTaxis() {
                log.info("获取锁Dispatcher:driveTaxis");
                for (Taxi t : availableTaxis) {
                    log.info("开到:【{}】", t.getLocation());
                }
            }
        }
    
        public static void main(String[] args) {
            Dispatcher dispatcher = new Dispatcher();
            Taxi A = new Taxi(dispatcher, "LA");
            new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    A.setLocation("LA");
                }
            }).start();
            new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    dispatcher.driveTaxis();
                }
            }).start();
        }
    }
    

    上例代码中,没有一个方法显示获取两个锁,但是方法调取需要再次获得锁,setLocation和driveTaxis本身是同步方法,在方法体内又需要获取锁,这样交替执行导致了锁顺序死锁。在LeftRightDeadlock、TransferMoneyDeadlock中,要查找死锁比较简单,只需要找到方法体内获取两个锁的地方。而出租车调度的案例中查找死锁比较困难,警惕在已经持有锁的方法内调用外部同步方法。

    解决办法就是使用开放调用,开放调用指调用该方法不需要持有锁。

    @Slf4j
    public class CooperateCallOpened {
        private static class Taxi {
            private String location;
            private String destination;
            private Dispatcher dispatcher;
    
            public Taxi(Dispatcher dispatcher, String destination) {
                this.dispatcher = dispatcher;
                this.destination = destination;
            }
    
            public synchronized String getLocation() {
                log.info("获取锁Taxi:getLocation");
                return location;
            }
    
            public void setLocation(String location) {
                boolean flag = false;
                synchronized (this) {
                    log.info("获取锁Taxi:setLocation");
                    this.location = location;
                    if (location.equals(destination)) {
                        flag = true;
                    }
                }
    
                if (flag) {
                    dispatcher.notifyAvailable(this);
                }
            }
        }
    
        private static class Dispatcher {
            private Set<Taxi> availableTaxis;
    
            public Dispatcher() {
                availableTaxis = new HashSet<>();
            }
    
            public synchronized void notifyAvailable(Taxi taxi) {
                log.info("获取锁Dispatcher:notifyAvailable");
                availableTaxis.add(taxi);
            }
    
            public void driveTaxis() {
                Set<Taxi> copy;
                synchronized (this) {
                    log.info("获取锁Dispatcher:driveTaxis");
                    copy = new HashSet<>(availableTaxis);
                }
                for (Taxi t : copy) {
                    log.info("开到:【{}】", t.getLocation());
                }
            }
        }
    
        public static void main(String[] args) {
            Dispatcher dispatcher = new Dispatcher();
            Taxi A = new Taxi(dispatcher, "LA");
            new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    A.setLocation("LA");
                }
            }).start();
            new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    dispatcher.driveTaxis();
                }
            }).start();
        }
    }
    
    

    相关文章

      网友评论

          本文标题:Java 死锁分类

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