java并发编程系列——死锁的解决

Posted 守夜人爱吃兔子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发编程系列——死锁的解决相关的知识,希望对你有一定的参考价值。

接下来我会以一个经典的转帐问题,跟大家一起聊聊死锁,以及如何解决这个问题。

假如客户 A 需要给客户 B 转账,客户 A 的账户减少 100 元,客户 B 的账户增加 100 元。

我们转化为代码描述:有一个账户类 Account,账户类有一个转账方法 transfer() 方法,该方法接收两个参数,转入账户和转账金额。

示例:1
public class Account {
    private int balance;

    public void transfer(Account target,int amt) {
        this.balance -= amt;
        target.balance += amt;
    }

}

实例 1 中有一个成员变量 balance,如何保证该成员变量在并发情况下没有线程安全性问题呢?我相信经过你的思考你会对代码做如下改进:

示例:2
public class Account {
    private int balance;

    public synchronized void transfer(Account target,int amt) {
        this.balance -= amt;
        target.balance += amt;
    }

}

那么加上 Synchronized 的方法有啥问题呢?我们在前面已经学习过了这种非静态方法,默认的锁是 this 也就是当前对象,但是如果我们仔细观察这段代码我们就会发现问题出在哪,代码区有两个资源,一个 this 表示转出账户,target 表示转入账户。而锁(this)只能锁住当前转出账户的变量 balance,无法锁住转入账户的 balance,所以此时如果有线程操作了账户 B(比如客户 B 取了 100 元)。

既然我们知道问题出现在哪了,当前转出账户的锁不能锁住两个资源,那么我们是不是可以用多把锁去锁住两个资源呢。

示例:3
public class Account {
    private int balance;    
    public void transfer(Account target,int amt) {
        synchronized(this) {
            synchronized(target) {
                this.balance -= amt;
                target.balance += amt;
            }
        }
    } 
}

看上去我们似乎解决 this 这把锁不能锁住两个资源的情况,确实我们解决了这个问题,但是新的问题又出现了。我们假设这样的场景,A 给 B 转账的同时,B 也在给 A 转账。

对应到代码层面的逻辑:

  • A 给 B 转账:首先获取对象 A 的锁,然后尝试去获取对象 B 的锁
  • B 给 A 转账:首先获取对象 B 的锁,然后尝试获取对象 A 的锁

此时我们可以看出,形成了死锁,A 转账时,拿了自己的锁尝试去获取 B 的锁,B 转账时拿了自己的锁尝试去获取 A 的锁,然后谁也拿不到对方的锁,这就是死锁。

死锁预防

在 Java 并发编程领域已经有技术大咖总结出了发生死锁的条件,只有四个条件都发生时才会出现死锁:

  1. 互斥,共享资源 X 和 Y 只能被一个线程占用
  2. 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X
  3. 不可抢占,其他线程不能强行抢占线程 T1 占有的资源
  4. 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待

只要能破坏其中一个,就可以成功避免死锁的发生,因为对于共享资源我们要得就是互斥的效果,所以第一个条件是无法破坏的,所以可以从下面三个条件出手,具体实现方式如下。

解决该死锁问题

调整获取锁的顺序

通过上面死锁分析,我们知道因为同时转账时,获取锁的顺序不一致导致了死锁问题,那么我们可以通过调整获取锁的顺序保持一致,那么死锁问题也就可以解决了。

  • A 转账给 B:A 获取自身的锁,再尝试获取 B 锁
  • B 转账给 A:B 首先去获取 A 的锁,再尝试获取 B 自己的锁

每个账户都有唯一的 Id,通常情况 id 也是递增的,我们可以通过 id 的大小来获取锁的顺序。

代码如下:

示例:4
public class Account {

    private int balance;    
    private int id;

    public void transfer(Account target,int amt) {
        Account first = this;
        Account second  = target;
        if(this.id < target.id) {
            first = target;
            second = this;
        }
        synchronized(first) {
            synchronized(second ) {
                this.balance -= amt;
                target.balance += amt;
            }
        }
    } 
}

我们通过调整获取锁的顺序,每次获取的锁都是 id 最大的,这样就能保证获取锁的顺序问题,从而避免了死锁。

假设账户 A 的 id 为 100,账户 B 的 id 为 101;

  • A 转账给 B:B.id>A.id 首先获取 B 的锁,然后尝试获取 A 的锁
  • B 转账给 A:B.id > A.id 首先尝试获取 B 的锁(等待)

设置超时放弃

我们在上面的编码中使用的是 Synchronized 内置锁,无法设置超时,如果需要设置超时我们可是使用显示锁 Lock。

示例:5
private Lock lock = new ReentrantLock();
    private int balance;

    public void transfer(Account target, int amt) {
        try {
            lock.tryLock(10, TimeUnit.MILLISECONDS);
            this.balance -= amt;
            target.balance += amt;
        } catch (InterruptedException e) {
        } finally {
            lock.unlock();
     }
}    

该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。

注:实际开发中这种都是用数据库事务 + 乐观锁的方式解决的。本例子只是为了演示死锁。

最后

一直想整理出一份完美的面试宝典,但是时间上一直腾不开,这套一千多道面试题宝典,结合今年金三银四各种大厂面试题,以及 GitHub 上 star 数超 30K+ 的文档整理出来的,我上传以后,毫无意外的短短半个小时点赞量就达到了 13k,说实话还是有点不可思议的。

需要完整版的小伙伴,可以一键三连后,点击这里

一千道互联网 Java 工程师面试题

内容涵盖:Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、Redis、mysql、Spring、SpringBoot、SpringCloud、RabbitMQ、Kafka、Linux等技术栈(485页)

《Java核心知识点合集(283页)》

内容涵盖:Java基础、JVM、高并发、多线程、分布式、设计模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat、数据库、云计算等

《Java中高级核心知识点合集(524页)》

《Java高级架构知识点整理》

 

 由于篇幅限制,详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

需要完整版的小伙伴,可以一键三连后,点击这里

以上是关于java并发编程系列——死锁的解决的主要内容,如果未能解决你的问题,请参考以下文章

java并发编程系列——死锁的解决

java并发编程系列——死锁的解决

Java并发编程艺术系列-一并发编程问题与解决

java并发编程死锁

并发编程--锁--如何使用命令行和代码定位死锁

Java并发编程:死锁(含代码)