前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文详解,死锁与解决方案(附源码)

一文详解,死锁与解决方案(附源码)

作者头像
小灰
发布2021-11-10 10:30:29
4350
发布2021-11-10 10:30:29
举报
文章被收录于专栏:程序员小灰程序员小灰

死锁的现象

想象一个场景,账户A给账户B转账,同时账户B也给账户A转账,两个账户都需要锁住余额,所以通常会申请两把锁,转账时,先锁住自己的账户,并获取对方的锁,保证同一时刻只能有一个线程去执行转账。

这时可能就会出现,对方给我转账,同时我也给对方转账,那么双方都持有自己的锁,且尝试去获取对方的锁,这就造成可能一直申请不到对方的锁,循环等待,就会发生“死锁”。

一旦发生死锁,线程一直占用着资源无法释放,又无法完成转账,就会造成系统假死。

什么是死锁?

“死锁”就是两个或两个以上的线程在执行过程中,互相持有对方所需要的资源,导致这些线程处于等待状态,无法继续执行。若无外力作用,它们都将无法继续执行下去,就进入了“永久”阻塞的状态。

图1 死锁的现象

如图所示,线程1获取了资源1,同时去请求获取资源2,但是线程2已经占有资源2了,所以线程1只能等待。同样的,线程2占有了资源2,要请求获取资源1,但资源1已经被线程1占有了,只能等待。于是线程1和线程2都在等待持有对方的持有的资源,就会无限等待下去,这就是死锁现象。

模拟发生死锁的场景

下面写一段代码,模拟两个线程各自持有了锁,然后请求获取对方持有的锁,发生死锁的现象。

代码语言:javascript
复制
public class DeadLock {
    public static String obj1 = "obj1";
    public static String obj2 = "obj2";

    public static void main(String[] args) {
        Thread a = new Thread(new Lock1());
        Thread b = new Thread(new Lock2());
        a.start();
        b.start();
    }

    static class Lock1 implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("Lock1 running");
                synchronized (DeadLock.obj1) {
                    System.out.println("Lock1 lock obj1");
                    Thread.sleep(5000);
                    synchronized (DeadLock.obj2) {
                        System.out.println("Lock1 lock obj2");
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    static class Lock2 implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("Lock2 running");
                synchronized (DeadLock.obj2) {
                    System.out.println("Lock2 lock obj2");
                    Thread.sleep(5000);
                    synchronized (DeadLock.obj1) {
                        System.out.println("Lock2 lock obj1");
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

程序启动后,从控制台输出,就能看出两个线程都没有结束,而是被卡住了。

图2 死锁demo输出

我们用jvisualVM看下线程的堆栈信息:

图3 jvisualVM堆栈信息

我们用jvisualVM查看线程的堆栈信息,发现已经检测到了死锁的存在,而且定位到了具体的代码行。

死锁产生的原因

死锁的发生也必须具备一定的条件,必须具备以下四个条件:

  • 互斥,共享资源 X 和 Y 只能被一个线程占用;
  • 占有且等待,线程01 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
  • 不可抢占,其他线程不能强行抢占线程01 占有的资源;
  • 循环等待,线程01 等待线程02 占有的资源,线程02 等待线程01 占有的资源,就是循环等待。
如何避免死锁?

死锁一旦发生,并没有什么好的方法解决,通常我们只能避免死锁的发生。

怎么避免呢?那就要看针对死锁发生的原因去解决。

  1. 首先,“互斥”是没有办法避免的,你想从账户A转账到账户B,就必须加锁,就没法避免互斥的存在。
  2. 对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。
  3. 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以在一定时间后,主动释放它占有的资源,这样就解决了不可抢占这个条件。
  4. 对于“循环等待”,我们可以靠按“次序”申请资源来预防。所谓按序申请,就是给资源设定顺序,申请的时候可以先申请序号小的资源,再申请序号大的,这样资源线性化后,自然就不存在循环等待了。

所以,总结来看,避免死锁的发生有三种方法:破坏占用且等待的条件、破坏不可抢占条件、破坏循环等待条件。

1、破坏占用且等待条件

我们要破坏占用且等待,就是一次性申请占有所有的资源。账户A给账户B转账,就可以一次性申请账户A和账户B的锁,同时拿到两个锁之后,在执行转账操作。

代码语言:javascript
复制
public class DeadLock2 {
    public static void main(String[] args) {
        Account a = new Account();
        Account b = new Account();
        a.transfer(b, 100);
        b.transfer(a, 200);
    }

    static class Allocator {
        private List<Account> als = new ArrayList<>();

        private void Allocator() {
        }

        synchronized boolean apply(Account from, Account to) {
            if (als.contains(from) || als.contains(to)) {
                return false;
            } else {
                als.add(from);
                als.add(to);
            }
            return true;
        }

        synchronized void clean(Account from, Account to) {
            als.remove(from);
            als.remove(to);
        }
    }

    static class Account {
        private Allocator actr = DeadLock2.getInstance();
        private int balance;

        void transfer(Account target, int amt) {
            while (!actr.apply(this, target)){
            }
            try {
                synchronized (this) {
                    System.out.println(this.toString() + " lock lock1");
                    synchronized (target) {
                        System.out.println(this.toString() + " lock lock2");
                        if (this.balance > amt) {
                            this.balance -= amt;
                            target.balance += amt;
                        }
                    }
                }
            } finally {
                actr.clean(this, target);
            }
        }
    }

    private static class SingleTonHoler {
        private static Allocator INSTANCE = new Allocator();
    }

    public static Allocator getInstance() {
        return SingleTonHoler.INSTANCE;
    }
}

输出结果如下:

图4 破坏占用且等待条件输出

从输出结果看出,并没有发生死锁,一个账户先获取了两把锁,完成转账后,另一个账号再获取到两把锁,完成转账。

上面的demo比较见到,如果账号没获取到锁,会一直while循环等待,可以优化为notify/wait的方式。

2、 破坏不可抢占条件

破坏不抢占条件,需要发生死锁的线程能够主动释放它占有的资源,但使用synchronized是做不到的。原因为synchronized申请不到资源时,线程直接进入了阻塞状态,而线程进入了阻塞状态也就没有办法释放它占有的资源了。

不过JDK中的Lock解决这个问题。

使用Lock类中的定时tryLock获取锁,可以指定一个超时时限(Timeout),在等待超过该时间后tryLock就会返回一个失败信息,也会释放其拥有的资源。

代码语言:javascript
复制
public class DeadLock3 {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        Thread a = new Thread(new Lock1());
        Thread b = new Thread(new Lock2());
        a.start();
        b.start();
    }

    static class Lock1 implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("Lock1 running");
                while (true) {
                    if (lock1.tryLock(1, TimeUnit.MILLISECONDS)) {
                        System.out.println("Lock1 get lock1");
                        if (lock2.tryLock(1, TimeUnit.MILLISECONDS)) {
                            System.out.println("Lock·get lock2");
                            return;
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock1.unlock();
                lock2.unlock();
            }
        }
    }

    static class Lock2 implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("Lock2 running");
                while (true) {
                    if (lock1.tryLock(1, TimeUnit.MILLISECONDS)) {
                        System.out.println("Lock2 get lock1");
                        if (lock2.tryLock(1, TimeUnit.MILLISECONDS)) {
                            System.out.println("Lock2 get lock2");
                            return;
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock1.unlock();
                lock2.unlock();
            }
        }
    }
}

输出结果如下:

图5 破坏不可抢占条件输出

从输出结果看出,并没有发生死锁,一个账户先尝试获取两把锁,如果超时没有获取到,就会下次重试再去获取,直到获取成功。

3、破坏循环等待条件

破坏循环等待,就是要对系统中的资源进行统一编号,进程必须按照资源的编号顺序提出。这样做就能保证系统不出现死锁。这就是“资源有序分配法”。代码如下:

代码语言:javascript
复制
class Account {
        private int id;
        private int balance;
        void transfer(Account target, int amt){
            Account left = this;
            Account right = target;
            if (this.id > target.id) {
                left = target;
                right = this;
            }
            synchronized(left){
                synchronized(right){
                    if (this.balance > amt){
                        this.balance -= amt;
                        target.balance += amt;
                    }
                }
            }
        }
    }

总结:

文章主要讲了死锁发生的原因以及解决方法,但我们平时写的代码,可能逻辑比这里的例子要复杂很多,如果产生了死锁,可能会比较难以定位到,所以我们平时写代码时,尽量不要把多个锁交织在一起。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-10-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员小灰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是死锁?
  • 死锁产生的原因
  • 如何避免死锁?
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档