Java 死锁分类

2019-03-06  本文已影响0人  南风nanfeng

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();
    }
}

上一篇下一篇

猜你喜欢

热点阅读