美文网首页
死锁问题

死锁问题

作者: From64KB | 来源:发表于2020-11-17 09:55 被阅读0次

    记得以前看过一个笑话:面试官问死锁怎么回事儿?我想了想,然后回答面试官说你先发offer给我,我再回答你。这就是现实中的死锁。
    代码中的死锁是当两个以上的运算单元,双方都在等待对方停止运行,以获取系统资源,但是没有一方提前退出时,就称为死锁。举个简单的例子,线程1先持有了锁A,准备再请求锁B,但是在这之前线程2持有了锁B准备请求锁A,这时双方就会造成死锁。

    上面这个简化过的例子只能用于理解死锁的形成,对于解决死锁而言参考价值有限。现实的代码中,锁的方式多种多样有方法上的synchronizedReentrantLock等等,当他们混在一起用的时候,解决起来就比较棘手了。比如下面一些例子:

        public void testLock() throws InterruptedException {
            ReentrantLock reentrantLock = new ReentrantLock();
            reentrantLock.lock();
            reentrantLock.unlock();
    
            ArrayBlockingQueue<Integer> integers = new ArrayBlockingQueue<>(16);
            integers.take();
    
            Semaphore semaphore = new Semaphore(1);
            semaphore.acquire();
            semaphore.release();
        }
    
        public synchronized void process() { //lock on `this`
    
        }
    
        public static synchronized void processStatic() { //lock on class
    
        }
    

    很多时候要直接从代码逻辑上找到死锁是很困难的。必须要实际遇到死锁并借助一些工具才能解决这个问题。

    1. 线程dump
      使用jstack <process id>或者kill -3 <process id>
      image.png
      可以看到Found one Java-level deadlock:和下面输出的详细信息。通过这些信息可以辅助解决死锁问题。
    2. 通过Java SDK提供的JVM死锁检查api:
        public void detectDeadlock() {
            ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
            long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
            boolean findDeadlock = deadlockedThreads != null && deadlockedThreads.length > 0;
            System.out.println("Find deadlock:" + findDeadlock);
        }
    

    当然为了能及时检查出死锁,需要开启一个后台线程并进行合适频率的轮询。

    上面的方法是在出现死锁时的辅助检查手段,有没有什么方法可以尽量在代码中避免出现死锁呢?有,获取锁的时候增加超时限制。

        public void lockWithTimeout() throws InterruptedException {
            ReentrantLock reentrantLock = new ReentrantLock();
            boolean acquire = reentrantLock.tryLock(2, TimeUnit.MINUTES);
    
            ArrayBlockingQueue<Integer> integers = new ArrayBlockingQueue<>(16);
            integers.poll(2, TimeUnit.MINUTES);
    
            Semaphore semaphore = new Semaphore(1);
            semaphore.tryAcquire(2, TimeUnit.MINUTES);
        }
    

    OK,让我们再来看一个例子,下面的代码是否会形成死锁呢?

        public void doTransfer() {
            Account account1 = new Account();
            Account account2 = new Account();
            //线程1
            new Thread(() -> transfer(account1, account2, 100)).start();
            //线程2
            new Thread(() -> transfer(account2, account1, 200)).start();//注意和上面的传参顺序相反
        }
    
        /**
         * 从account2转账至account1
         *
         * @param account1
         * @param account2
         * @param amount
         */
        public void transfer(Account account1, Account account2, int amount) {
            synchronized (account1) {
                synchronized (account2) {
                    account1.add(amount);
                    account2.reduce(amount);
                }
            }
        }
    
        class Account {
    
            public void add(int amount) {
    
            }
    
            public void reduce(int amount) {
    
            }
        }
    

    这样是会形成死锁的。线程1如果刚走到synchronized (account1)这里,而线程2也走到synchronized (account1)这里,由于account1的实际传入对象分别为account1account2,这时再向下走就会形成死锁。那怎么改上面的代码就可以避免死锁呢?下面是修改过的代码:

        public void doTransfer() {
            Account account1 = new Account();
            Account account2 = new Account();
            new Thread(() -> transfer(account1, account2, 100)).start();
            new Thread(() -> transfer(account2, account1, 200)).start();
        }
    
        /**
         * 从account2转账至account1
         *
         * @param account1
         * @param account2
         * @param amount
         */
        public void transfer(Account account1, Account account2, int amount) {
            //对account1和account2排序,使无论account1和account2如何传入在加锁时顺序一致
            Account firstAccount = getFirstAccount(account1, account2);
            Account secondAccount = getSecondAccount(account1, account2);
            synchronized (firstAccount) {
                synchronized (secondAccount) {
                    account1.add(amount);
                    account2.reduce(amount);
                }
            }
        }
    
        private Account getFirstAccount(Account account1, Account account2) {
            boolean b = account1.getId() > account2.getId();//仅作示例,表示account1某种固定属性可以和account2比较
            if (b) {
                return account1;
            } else {
                return account2;
            }
        }
    
        private Account getSecondAccount(Account account1, Account account2) {
            boolean b = account1.getId() < account2.getId();//仅作示例,表示account1某种固定属性可以和account2比较
            if (b) {
                return account1;
            } else {
                return account2;
            }
        }
    
        class Account {
    
            public void add(int amount) {
    
            }
    
            public void reduce(int amount) {
    
            }
    
            public int getId() {
                return 1;
            }
        }
    

    通过保持传入对象加锁顺序一致就可避免死锁问题。

    相关文章

      网友评论

          本文标题:死锁问题

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